From 2f0240c3066cda413d1bbaf8ff907657e72dfdaf Mon Sep 17 00:00:00 2001 From: William Bourque Date: Wed, 8 Jan 2014 20:26:08 +0000 Subject: [PATCH 1/1] HUE-1841. Add support for OpenId authentication --- desktop/Makefile | 3 +- desktop/conf.dist/hue.ini | 20 + .../core/ext-py/django-openid-auth-0.5/LICENSE.txt | 23 + .../core/ext-py/django-openid-auth-0.5/MANIFEST.in | 8 + .../core/ext-py/django-openid-auth-0.5/Makefile | 10 + .../core/ext-py/django-openid-auth-0.5/PKG-INFO | 31 + .../core/ext-py/django-openid-auth-0.5/README.txt | 179 ++ .../core/ext-py/django-openid-auth-0.5/TODO.txt | 0 .../django_openid_auth/__init__.py | 29 + .../django_openid_auth/admin.py | 90 + .../django_openid_auth/auth.py | 330 +++ .../django_openid_auth/exceptions.py | 68 + .../django_openid_auth/forms.py | 87 + .../django_openid_auth/management/__init__.py | 27 + .../management/commands/__init__.py | 27 + .../management/commands/openid_cleanup.py | 39 + .../django_openid_auth/models.py | 58 + .../django_openid_auth/signals.py | 34 + .../django_openid_auth/store.py | 131 ++ .../django_openid_auth/teams.py | 411 ++++ .../templates/openid/failure.html | 11 + .../django_openid_auth/templates/openid/login.html | 43 + .../django_openid_auth/tests/__init__.py | 41 + .../django_openid_auth/tests/test_admin.py | 88 + .../django_openid_auth/tests/test_auth.py | 185 ++ .../django_openid_auth/tests/test_store.py | 193 ++ .../django_openid_auth/tests/test_views.py | 1376 +++++++++++++ .../django_openid_auth/tests/urls.py | 39 + .../django_openid_auth/urls.py | 36 + .../django_openid_auth/views.py | 313 +++ .../example_consumer/__init__.py | 28 + .../example_consumer/manage.py | 11 + .../example_consumer/settings.py | 145 ++ .../example_consumer/urls.py | 45 + .../example_consumer/views.py | 57 + .../core/ext-py/django-openid-auth-0.5/setup.py | 80 + desktop/core/ext-py/python-openid-2.2.5/LICENSE | 202 ++ .../core/ext-py/python-openid-2.2.5/MANIFEST.in | 7 + desktop/core/ext-py/python-openid-2.2.5/NEWS | 225 +++ desktop/core/ext-py/python-openid-2.2.5/NOTICE | 4 + desktop/core/ext-py/python-openid-2.2.5/PKG-INFO | 25 + desktop/core/ext-py/python-openid-2.2.5/README | 68 + .../python-openid-2.2.5/admin/builddiscover.py | 67 + .../core/ext-py/python-openid-2.2.5/admin/fixperms | 10 + .../ext-py/python-openid-2.2.5/admin/gettlds.py | 47 + .../ext-py/python-openid-2.2.5/admin/makechangelog | 2 + .../core/ext-py/python-openid-2.2.5/admin/makedoc | 7 + .../ext-py/python-openid-2.2.5/admin/pythonsource | 2 + .../core/ext-py/python-openid-2.2.5/admin/runtests | 204 ++ .../ext-py/python-openid-2.2.5/admin/setversion | 7 + .../ext-py/python-openid-2.2.5/admin/tagrelease | 13 + .../background-associations.txt | 96 + .../ext-py/python-openid-2.2.5/contrib/associate | 47 + .../python-openid-2.2.5/contrib/openid-parse | 151 ++ .../contrib/upgrade-store-1.1-to-2.0 | 170 ++ .../ext-py/python-openid-2.2.5/examples/README | 91 + .../python-openid-2.2.5/examples/__init__.py | 0 .../python-openid-2.2.5/examples/consumer.py | 507 +++++ .../ext-py/python-openid-2.2.5/examples/discover | 46 + .../python-openid-2.2.5/examples/djopenid/README | 67 + .../examples/djopenid/__init__.py | 0 .../examples/djopenid/consumer/__init__.py | 0 .../examples/djopenid/consumer/models.py | 3 + .../examples/djopenid/consumer/urls.py | 9 + .../examples/djopenid/consumer/views.py | 220 ++ .../examples/djopenid/manage.py | 11 + .../examples/djopenid/server/__init__.py | 0 .../examples/djopenid/server/models.py | 3 + .../examples/djopenid/server/tests.py | 103 + .../examples/djopenid/server/urls.py | 12 + .../examples/djopenid/server/views.py | 279 +++ .../examples/djopenid/settings.py | 84 + .../djopenid/templates/consumer/index.html | 127 ++ .../djopenid/templates/consumer/request_form.html | 5 + .../examples/djopenid/templates/index.html | 28 + .../djopenid/templates/server/endpoint.html | 15 + .../examples/djopenid/templates/server/idPage.html | 14 + .../examples/djopenid/templates/server/index.html | 51 + .../templates/server/pape_request_info.html | 21 + .../examples/djopenid/templates/server/trust.html | 51 + .../examples/djopenid/templates/xrds.xml | 17 + .../python-openid-2.2.5/examples/djopenid/urls.py | 8 + .../python-openid-2.2.5/examples/djopenid/util.py | 147 ++ .../python-openid-2.2.5/examples/djopenid/views.py | 14 + .../ext-py/python-openid-2.2.5/examples/server.py | 721 +++++++ .../ext-py/python-openid-2.2.5/openid/__init__.py | 55 + .../python-openid-2.2.5/openid/association.py | 555 ++++++ .../openid/consumer/__init__.py | 6 + .../openid/consumer/consumer.py | 1900 ++++++++++++++++++ .../openid/consumer/discover.py | 470 +++++ .../openid/consumer/html_parse.py | 249 +++ .../ext-py/python-openid-2.2.5/openid/cryptutil.py | 220 ++ .../core/ext-py/python-openid-2.2.5/openid/dh.py | 42 + .../ext-py/python-openid-2.2.5/openid/extension.py | 46 + .../openid/extensions/__init__.py | 5 + .../python-openid-2.2.5/openid/extensions/ax.py | 774 ++++++++ .../openid/extensions/draft/__init__.py | 0 .../openid/extensions/draft/pape2.py | 277 +++ .../openid/extensions/draft/pape5.py | 473 +++++ .../python-openid-2.2.5/openid/extensions/sreg.py | 518 +++++ .../ext-py/python-openid-2.2.5/openid/fetchers.py | 427 ++++ .../ext-py/python-openid-2.2.5/openid/kvform.py | 123 ++ .../ext-py/python-openid-2.2.5/openid/message.py | 631 ++++++ .../ext-py/python-openid-2.2.5/openid/oidutil.py | 190 ++ .../python-openid-2.2.5/openid/server/__init__.py | 6 + .../python-openid-2.2.5/openid/server/server.py | 1849 +++++++++++++++++ .../python-openid-2.2.5/openid/server/trustroot.py | 454 +++++ .../core/ext-py/python-openid-2.2.5/openid/sreg.py | 7 + .../python-openid-2.2.5/openid/store/__init__.py | 8 + .../python-openid-2.2.5/openid/store/filestore.py | 426 ++++ .../python-openid-2.2.5/openid/store/interface.py | 197 ++ .../python-openid-2.2.5/openid/store/memstore.py | 125 ++ .../python-openid-2.2.5/openid/store/nonce.py | 98 + .../python-openid-2.2.5/openid/store/sqlstore.py | 516 +++++ .../python-openid-2.2.5/openid/test/__init__.py | 0 .../python-openid-2.2.5/openid/test/cryptutil.py | 108 + .../openid/test/data/accept.txt | 118 ++ .../openid/test/data/example-xrds.xml | 14 + .../test/data/openid-1.2-consumer-sqlitestore.db | Bin 0 -> 7168 bytes .../openid/test/data/test1-discover.txt | 137 ++ .../openid/test/data/test1-parsehtml.txt | 152 ++ .../openid/test/data/test_discover/openid.html | 11 + .../openid/test/data/test_discover/openid2.html | 11 + .../test/data/test_discover/openid2_xrds.xml | 12 + .../test_discover/openid2_xrds_no_local_id.xml | 11 + .../test/data/test_discover/openid_1_and_2.html | 11 + .../data/test_discover/openid_1_and_2_xrds.xml | 16 + .../openid_1_and_2_xrds_bad_delegate.xml | 17 + .../test/data/test_discover/openid_and_yadis.html | 12 + .../data/test_discover/openid_no_delegate.html | 10 + .../test/data/test_discover/yadis_0entries.xml | 12 + .../data/test_discover/yadis_2_bad_local_id.xml | 15 + .../data/test_discover/yadis_2entries_delegate.xml | 22 + .../test/data/test_discover/yadis_2entries_idp.xml | 21 + .../data/test_discover/yadis_another_delegate.xml | 14 + .../openid/test/data/test_discover/yadis_idp.xml | 12 + .../test/data/test_discover/yadis_idp_delegate.xml | 13 + .../test/data/test_discover/yadis_no_delegate.xml | 11 + .../openid/test/data/test_etxrd/README | 12 + .../data/test_etxrd/delegated-20060809-r1.xrds | 34 + .../data/test_etxrd/delegated-20060809-r2.xrds | 34 + .../test/data/test_etxrd/delegated-20060809.xrds | 34 + .../openid/test/data/test_etxrd/no-xrd.xml | 7 + .../openid/test/data/test_etxrd/not-xrds.xml | 2 + .../test/data/test_etxrd/prefixsometimes.xrds | 34 + .../openid/test/data/test_etxrd/ref.xrds | 109 + .../test/data/test_etxrd/sometimesprefix.xrds | 34 + .../openid/test/data/test_etxrd/spoof1.xrds | 25 + .../openid/test/data/test_etxrd/spoof2.xrds | 25 + .../openid/test/data/test_etxrd/spoof3.xrds | 37 + .../openid/test/data/test_etxrd/status222.xrds | 9 + .../openid/test/data/test_etxrd/subsegments.xrds | 58 + .../test/data/test_etxrd/valid-populated-xrds.xml | 39 + .../openid/test/data/trustroot.txt | 150 ++ .../python-openid-2.2.5/openid/test/datadriven.py | 47 + .../ext-py/python-openid-2.2.5/openid/test/dh.py | 70 + .../ext-py/python-openid-2.2.5/openid/test/dhpriv | 29 + .../openid/test/discoverdata.py | 125 ++ .../python-openid-2.2.5/openid/test/kvform.py | 174 ++ .../python-openid-2.2.5/openid/test/linkparse.py | 109 + .../python-openid-2.2.5/openid/test/linkparse.txt | 584 ++++++ .../ext-py/python-openid-2.2.5/openid/test/n2b64 | 650 ++++++ .../python-openid-2.2.5/openid/test/oidutil.py | 176 ++ .../python-openid-2.2.5/openid/test/storetest.py | 397 ++++ .../python-openid-2.2.5/openid/test/support.py | 51 + .../python-openid-2.2.5/openid/test/test_accept.py | 127 ++ .../openid/test/test_association.py | 183 ++ .../openid/test/test_association_response.py | 340 ++++ .../openid/test/test_auth_request.py | 206 ++ .../python-openid-2.2.5/openid/test/test_ax.py | 626 ++++++ .../openid/test/test_consumer.py | 2097 ++++++++++++++++++++ .../openid/test/test_discover.py | 783 ++++++++ .../python-openid-2.2.5/openid/test/test_etxrd.py | 194 ++ .../openid/test/test_examples.py | 185 ++ .../openid/test/test_extension.py | 36 + .../openid/test/test_fetchers.py | 285 +++ .../openid/test/test_htmldiscover.py | 21 + .../openid/test/test_message.py | 998 ++++++++++ .../openid/test/test_negotiation.py | 271 +++ .../python-openid-2.2.5/openid/test/test_nonce.py | 104 + .../openid/test/test_openidyadis.py | 164 ++ .../python-openid-2.2.5/openid/test/test_pape.py | 9 + .../openid/test/test_pape_draft2.py | 217 ++ .../openid/test/test_pape_draft5.py | 441 ++++ .../openid/test/test_parsehtml.py | 82 + .../openid/test/test_rpverify.py | 246 +++ .../python-openid-2.2.5/openid/test/test_server.py | 2064 +++++++++++++++++++ .../openid/test/test_services.py | 23 + .../python-openid-2.2.5/openid/test/test_sreg.py | 484 +++++ .../python-openid-2.2.5/openid/test/test_symbol.py | 35 + .../openid/test/test_urinorm.py | 52 + .../openid/test/test_verifydisco.py | 270 +++ .../python-openid-2.2.5/openid/test/test_xri.py | 102 + .../python-openid-2.2.5/openid/test/test_xrires.py | 40 + .../openid/test/test_yadis_discover.py | 179 ++ .../python-openid-2.2.5/openid/test/trustroot.py | 85 + .../python-openid-2.2.5/openid/test/urinorm.txt | 87 + .../ext-py/python-openid-2.2.5/openid/urinorm.py | 202 ++ .../python-openid-2.2.5/openid/yadis/__init__.py | 26 + .../python-openid-2.2.5/openid/yadis/accept.py | 133 ++ .../python-openid-2.2.5/openid/yadis/constants.py | 13 + .../python-openid-2.2.5/openid/yadis/discover.py | 135 ++ .../python-openid-2.2.5/openid/yadis/etxrd.py | 300 +++ .../python-openid-2.2.5/openid/yadis/filters.py | 200 ++ .../python-openid-2.2.5/openid/yadis/manager.py | 194 ++ .../python-openid-2.2.5/openid/yadis/parsehtml.py | 197 ++ .../python-openid-2.2.5/openid/yadis/services.py | 54 + .../ext-py/python-openid-2.2.5/openid/yadis/xri.py | 168 ++ .../python-openid-2.2.5/openid/yadis/xrires.py | 123 ++ desktop/core/ext-py/python-openid-2.2.5/setup.cfg | 3 + desktop/core/ext-py/python-openid-2.2.5/setup.py | 46 + desktop/core/src/desktop/settings.py | 9 + desktop/core/src/desktop/urls.py | 4 + desktop/libs/libopenid/Makefile | 36 + desktop/libs/libopenid/babel.cfg | 2 + desktop/libs/libopenid/setup.py | 30 + desktop/libs/libopenid/src/libopenid/__init__.py | 0 desktop/libs/libopenid/src/libopenid/backend.py | 84 + desktop/libs/libopenid/src/libopenid/conf.py | 68 + desktop/libs/libopenid/src/libopenid/forms.py | 14 + .../src/libopenid/locale/en/LC_MESSAGES/django.po | 28 + .../libs/libopenid/src/libopenid/locale/en_US.pot | 27 + .../libopenid/src/libopenid/openid_settings.py | 45 + .../src/libopenid/templates/openid-login.html | 175 ++ desktop/libs/libopenid/src/libopenid/urls.py | 24 + desktop/libs/libopenid/src/libopenid/views.py | 63 + 226 files changed, 36204 insertions(+), 1 deletion(-) create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/LICENSE.txt create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/MANIFEST.in create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/Makefile create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/PKG-INFO create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/README.txt create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/TODO.txt create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/__init__.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/admin.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/auth.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/exceptions.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/forms.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/management/__init__.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/management/commands/__init__.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/management/commands/openid_cleanup.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/models.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/signals.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/store.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/teams.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/templates/openid/failure.html create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/templates/openid/login.html create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/__init__.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_admin.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_auth.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_store.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_views.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/urls.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/urls.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/views.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/example_consumer/__init__.py create mode 100755 desktop/core/ext-py/django-openid-auth-0.5/example_consumer/manage.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/example_consumer/settings.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/example_consumer/urls.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/example_consumer/views.py create mode 100644 desktop/core/ext-py/django-openid-auth-0.5/setup.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/LICENSE create mode 100644 desktop/core/ext-py/python-openid-2.2.5/MANIFEST.in create mode 100644 desktop/core/ext-py/python-openid-2.2.5/NEWS create mode 100644 desktop/core/ext-py/python-openid-2.2.5/NOTICE create mode 100644 desktop/core/ext-py/python-openid-2.2.5/PKG-INFO create mode 100644 desktop/core/ext-py/python-openid-2.2.5/README create mode 100644 desktop/core/ext-py/python-openid-2.2.5/admin/builddiscover.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/admin/fixperms create mode 100644 desktop/core/ext-py/python-openid-2.2.5/admin/gettlds.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/admin/makechangelog create mode 100644 desktop/core/ext-py/python-openid-2.2.5/admin/makedoc create mode 100644 desktop/core/ext-py/python-openid-2.2.5/admin/pythonsource create mode 100644 desktop/core/ext-py/python-openid-2.2.5/admin/runtests create mode 100644 desktop/core/ext-py/python-openid-2.2.5/admin/setversion create mode 100644 desktop/core/ext-py/python-openid-2.2.5/admin/tagrelease create mode 100644 desktop/core/ext-py/python-openid-2.2.5/background-associations.txt create mode 100755 desktop/core/ext-py/python-openid-2.2.5/contrib/associate create mode 100644 desktop/core/ext-py/python-openid-2.2.5/contrib/openid-parse create mode 100644 desktop/core/ext-py/python-openid-2.2.5/contrib/upgrade-store-1.1-to-2.0 create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/README create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/__init__.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/consumer.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/discover create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/README create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/__init__.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/__init__.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/models.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/urls.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/views.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/manage.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/__init__.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/models.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/tests.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/urls.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/views.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/settings.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/consumer/index.html create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/consumer/request_form.html create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/index.html create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/endpoint.html create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/idPage.html create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/index.html create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/pape_request_info.html create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/trust.html create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/xrds.xml create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/urls.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/util.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/views.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/examples/server.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/__init__.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/association.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/consumer/__init__.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/consumer/consumer.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/consumer/discover.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/consumer/html_parse.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/cryptutil.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/dh.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/extension.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/extensions/__init__.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/extensions/ax.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/extensions/draft/__init__.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/extensions/draft/pape2.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/extensions/draft/pape5.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/extensions/sreg.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/fetchers.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/kvform.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/message.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/oidutil.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/server/__init__.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/server/server.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/server/trustroot.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/sreg.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/store/__init__.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/store/filestore.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/store/interface.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/store/memstore.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/store/nonce.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/store/sqlstore.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/__init__.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/cryptutil.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/accept.txt create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/example-xrds.xml create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/openid-1.2-consumer-sqlitestore.db create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test1-discover.txt create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test1-parsehtml.txt create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_discover/openid.html create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_discover/openid2.html create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_discover/openid2_xrds.xml create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_discover/openid2_xrds_no_local_id.xml create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_discover/openid_1_and_2.html create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_discover/openid_1_and_2_xrds.xml create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_discover/openid_1_and_2_xrds_bad_delegate.xml create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_discover/openid_and_yadis.html create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_discover/openid_no_delegate.html create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_discover/yadis_0entries.xml create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_discover/yadis_2_bad_local_id.xml create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_discover/yadis_2entries_delegate.xml create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_discover/yadis_2entries_idp.xml create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_discover/yadis_another_delegate.xml create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_discover/yadis_idp.xml create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_discover/yadis_idp_delegate.xml create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_discover/yadis_no_delegate.xml create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_etxrd/README create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_etxrd/delegated-20060809-r1.xrds create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_etxrd/delegated-20060809-r2.xrds create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_etxrd/delegated-20060809.xrds create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_etxrd/no-xrd.xml create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_etxrd/not-xrds.xml create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_etxrd/prefixsometimes.xrds create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_etxrd/ref.xrds create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_etxrd/sometimesprefix.xrds create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_etxrd/spoof1.xrds create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_etxrd/spoof2.xrds create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_etxrd/spoof3.xrds create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_etxrd/status222.xrds create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_etxrd/subsegments.xrds create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test_etxrd/valid-populated-xrds.xml create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/data/trustroot.txt create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/datadriven.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/dh.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/dhpriv create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/discoverdata.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/kvform.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/linkparse.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/linkparse.txt create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/n2b64 create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/oidutil.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/storetest.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/support.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_accept.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_association.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_association_response.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_auth_request.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_ax.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_consumer.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_discover.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_etxrd.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_examples.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_extension.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_fetchers.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_htmldiscover.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_message.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_negotiation.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_nonce.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_openidyadis.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_pape.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_pape_draft2.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_pape_draft5.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_parsehtml.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_rpverify.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_server.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_services.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_sreg.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_symbol.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_urinorm.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_verifydisco.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_xri.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_xrires.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/test_yadis_discover.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/trustroot.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/test/urinorm.txt create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/urinorm.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/yadis/__init__.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/yadis/accept.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/yadis/constants.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/yadis/discover.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/yadis/etxrd.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/yadis/filters.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/yadis/manager.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/yadis/parsehtml.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/yadis/services.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/yadis/xri.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/openid/yadis/xrires.py create mode 100644 desktop/core/ext-py/python-openid-2.2.5/setup.cfg create mode 100644 desktop/core/ext-py/python-openid-2.2.5/setup.py create mode 100644 desktop/libs/libopenid/Makefile create mode 100644 desktop/libs/libopenid/babel.cfg create mode 100644 desktop/libs/libopenid/setup.py create mode 100644 desktop/libs/libopenid/src/libopenid/__init__.py create mode 100644 desktop/libs/libopenid/src/libopenid/backend.py create mode 100644 desktop/libs/libopenid/src/libopenid/conf.py create mode 100644 desktop/libs/libopenid/src/libopenid/forms.py create mode 100644 desktop/libs/libopenid/src/libopenid/locale/en/LC_MESSAGES/django.po create mode 100644 desktop/libs/libopenid/src/libopenid/locale/en_US.pot create mode 100644 desktop/libs/libopenid/src/libopenid/openid_settings.py create mode 100644 desktop/libs/libopenid/src/libopenid/templates/openid-login.html create mode 100644 desktop/libs/libopenid/src/libopenid/urls.py create mode 100644 desktop/libs/libopenid/src/libopenid/views.py diff --git a/desktop/Makefile b/desktop/Makefile index c98a990..8ff9744 100644 --- a/desktop/Makefile +++ b/desktop/Makefile @@ -43,7 +43,8 @@ APPS := core \ libs/hadoop \ libs/liboozie \ libs/libsaml \ - libs/librdbms + libs/librdbms \ + libs/libopenid .PHONY: default default:: hue syncdb diff --git a/desktop/conf.dist/hue.ini b/desktop/conf.dist/hue.ini index 89ba7d8..f296840 100644 --- a/desktop/conf.dist/hue.ini +++ b/desktop/conf.dist/hue.ini @@ -97,6 +97,7 @@ # - desktop.auth.backend.RemoteUserDjangoBackend # - desktop.auth.backend.OAuthBackend # - libsaml.backend.SAML2Backend + # - libopenid.backend.OpenIDBackend ## backend=desktop.auth.backend.AllowFirstUserDjangoBackend # Backend to synchronize user-group membership with @@ -311,6 +312,25 @@ ########################################################################### +# Settings to configure OPENID +########################################################################### + +[libopenid] + # (Required) OpenId SSO endpoint url. + ## server_endpoint_url=https://www.google.com/accounts/o8/id + + # OpenId 1.1 identity url prefix to be used instead of SSO endpoint url + # This is only supported if you are using an OpenId 1.1 endpoint + ## identity_url_prefix=https://app.onelogin.com/openid/your_company.com/ + + # Create users from OPENID on login. + ## create_users_on_login=true + + # Use email for username + ## use_email_for_username=true + + +########################################################################### # Settings for the RDBMS application ########################################################################### diff --git a/desktop/core/ext-py/django-openid-auth-0.5/LICENSE.txt b/desktop/core/ext-py/django-openid-auth-0.5/LICENSE.txt new file mode 100644 index 0000000..3abea62 --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/LICENSE.txt @@ -0,0 +1,23 @@ +Copyright (C) 2007 Simon Willison +Copyright (C) 2008-2010 Canonical Ltd. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/desktop/core/ext-py/django-openid-auth-0.5/MANIFEST.in b/desktop/core/ext-py/django-openid-auth-0.5/MANIFEST.in new file mode 100644 index 0000000..acc52f6 --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/MANIFEST.in @@ -0,0 +1,8 @@ +include Makefile +include MANIFEST.in +include LICENSE.txt +include README.txt +include TODO.txt + +recursive-include django_openid_auth/templates *.html +recursive-include example_consumer *.py diff --git a/desktop/core/ext-py/django-openid-auth-0.5/Makefile b/desktop/core/ext-py/django-openid-auth-0.5/Makefile new file mode 100644 index 0000000..d43e6cb --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/Makefile @@ -0,0 +1,10 @@ + +check: + PYTHONPATH=$(shell pwd) python example_consumer/manage.py test \ + --verbosity=2 django_openid_auth + +run-example-consumer: + PYTHONPATH=$(shell pwd) python example_consumer/manage.py syncdb + PYTHONPATH=$(shell pwd) python example_consumer/manage.py runserver + +.PHONY: check run-example-consumer diff --git a/desktop/core/ext-py/django-openid-auth-0.5/PKG-INFO b/desktop/core/ext-py/django-openid-auth-0.5/PKG-INFO new file mode 100644 index 0000000..d669d54 --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/PKG-INFO @@ -0,0 +1,31 @@ +Metadata-Version: 1.1 +Name: django-openid-auth +Version: 0.5 +Summary: OpenID integration for django.contrib.auth +Home-page: https://launchpad.net/django-openid-auth +Author: Canonical Ltd +Author-email: UNKNOWN +License: BSD +Download-URL: http://launchpad.net/django-openid-auth/trunk/0.5/+download/django-openid-auth-0.5.tar.gz +Description: A library that can be used to add OpenID support to Django applications. + The library integrates with Django's built in authentication system, so + most applications require minimal changes to support OpenID llogin. The + library also includes the following features: + * Basic user details are transferred from the OpenID server via the + Simple Registration extension or Attribute Exchange extension. + * can be configured to use a fixed OpenID server URL, for use in SSO. + * supports the launchpad.net teams extension to get team membership + info. + +Platform: any +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Web Environment +Classifier: Framework :: Django +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires: django (>=1.1.2) +Requires: openid (>=2.2.0) +Provides: django_openid_auth diff --git a/desktop/core/ext-py/django-openid-auth-0.5/README.txt b/desktop/core/ext-py/django-openid-auth-0.5/README.txt new file mode 100644 index 0000000..c16e8bf --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/README.txt @@ -0,0 +1,179 @@ += Django OpenID Authentication Support = + +This package provides integration between Django's authentication +system and OpenID authentication. It also includes support for using +a fixed OpenID server endpoint, which can be useful when implementing +single signon systems. + + +== Basic Installation == + + 1. Install the Jan Rain Python OpenID library. It can be found at: + + http://openidenabled.com/python-openid/ + + It can also be found in most Linux distributions packaged as + "python-openid". You will need version 2.2.0 or later. + + 2. Add 'django_openid_auth' to INSTALLED_APPS for your application. + At a minimum, you'll need the following in there: + + INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django_openid_auth', + ) + + 3. Add 'django_auth_openid.auth.OpenIDBackend' to + AUTHENTICATION_BACKENDS. This should be in addition to the + default ModelBackend: + + AUTHENTICATION_BACKENDS = ( + 'django_openid_auth.auth.OpenIDBackend', + 'django.contrib.auth.backends.ModelBackend', + ) + + 4. To create users automatically when a new OpenID is used, add the + following to the settings: + + OPENID_CREATE_USERS = True + + 5. To have user details updated from OpenID Simple Registration or + Attribute Exchange extension data each time they log in, add the + following: + + OPENID_UPDATE_DETAILS_FROM_SREG = True + + 6. Hook up the login URLs to your application's urlconf with + something like: + + urlpatterns = patterns('', + ... + (r'^openid/', include('django_openid_auth.urls')), + ... + ) + + 7. Configure the LOGIN_URL and LOGIN_REDIRECT_URL appropriately for + your site: + + LOGIN_URL = '/openid/login/' + LOGIN_REDIRECT_URL = '/' + + This will allow pages that use the standard @login_required + decorator to use the OpenID login page. + + 8. Rerun "python manage.py syncdb" to add the UserOpenID table to + your database. + + +== Configuring Single Sign-On == + +If you only want to accept identities from a single OpenID server and +that server implemnts OpenID 2.0 identifier select mode, add the +following setting to your app: + + OPENID_SSO_SERVER_URL = 'server-endpoint-url' + +With this setting enabled, the user will not be prompted to enter +their identity URL, and instead an OpenID authentication request will +be started with the given server URL. + +As an example, to use Launchpad accounts for SSO, you'd use: + + OPENID_SSO_SERVER_URL = 'https://login.launchpad.net/' + + +== Launchpad Teams Support == + +This library supports the Launchpad Teams OpenID extension. Using +this feature, it is possible to map Launchpad team memberships to +Django group memberships. It can be configured with: + + OPENID_SSO_SERVER_URL = 'https://login.launchpad.net/' + OPENID_LAUNCHPAD_TEAMS_MAPPING = { + 'launchpad-team-1': 'django-group-1', + 'launchpad-team-2': 'django-group-2', + } + +When a user logs in, they will be added or removed from the relevant +teams listed in the mapping. + +If you have already django-groups and want to map these groups automatically, you can use the OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO variable in your settings.py file. + + OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO = True + +If you use OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO, the variable OPENID_LAUNCHPAD_TEAMS_MAPPING will be ignored. +If you want to exclude some groups from the auto mapping, use OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST. This variable has only an effect if OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO is True. + + OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST = ['django-group1', 'django-group2'] + +== External redirect domains == + +By default, redirecting back to an external URL after auth is forbidden. To permit redirection to external URLs on a separate domain, define ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS in your settings.py file as a list of permitted domains: + + ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = ['example.com', 'example.org'] + +and redirects to external URLs on those domains will additionally be permitted. + +== Use as /admin (django.admin.contrib) login == + +If you require openid authentication into the admin application, add the following setting: + + OPENID_USE_AS_ADMIN_LOGIN = True + +It is worth noting that a user needs to be be marked as a "staff user" to be able to access the admin interface. A new openid user will not normally be a "staff user". +The easiest way to resolve this is to use traditional authentication (OPENID_USE_AS_ADMIN_LOGIN = False) to sign in as your first user with a password and authorise your +openid user to be staff. + +== Change Django usernames if the nickname changes on the provider == + +If you want your Django username to change when a user updates the nickname on their provider, add the following setting: + + OPENID_FOLLOW_RENAMES = True + +If the new nickname is available as a Django username, the user is renamed. +Otherwise the user will be renamed to nickname+i for an incrememnting value of i until no conflict occurs. +If the user has already been renamed to nickname+1 due to a conflict, and the nickname is still not available, the user will keep their existing username. + +== Require a valid nickname == + +If you must have a valid, unique nickname in order to create a user accont, add the following setting: + + OPENID_STRICT_USERNAMES = True + +This will cause an OpenID login attempt to fail if the provider does not return a 'nickname' (username) for the user, or if the nickname conflicts with an existing user with a different openid identiy url. +Without this setting, logins without a nickname will be given the username 'openiduser', and upon conflicts with existing username, an incrementing number will be appended to the username until it is unique. + +== Require Physical Multi-Factor Authentication == + +If your users should use a physical multi-factor authentication method, such as RSA tokens or YubiKey, add the following setting: + + OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = True + +If the user's OpenID provider supports the PAPE extension and provides the Physical Multifactor authentication policy, this will +cause the OpenID login to fail if the user does not provide valid physical authentication to the provider. + +== Override Login Failure Handling == + +You can optionally provide your own handler for login failures by adding the following setting: + + OPENID_RENDER_FAILURE = failure_handler_function + +Where failure_handler_function is a function reference that will take the following parameters: + + def failure_handler_function(request, message, status=None, template_name=None, exception=None) + +This function must return a Django.http.HttpResponse instance. + +== Use the user's email for suggested usernames == + +You can optionally strip out non-alphanumeric characters from the user's email +to generate a preferred username, if the server doesn't provide nick +information, by setting the following setting: + + OPENID_USE_EMAIL_FOR_USERNAME = True + +Otherwise, and by default, if the server omits nick information and a user is +created it'll receive a username 'openiduser' + a number. +Consider also the OPENID_STRICT_USERNAMES setting (see ``Require a valid nickname``) diff --git a/desktop/core/ext-py/django-openid-auth-0.5/TODO.txt b/desktop/core/ext-py/django-openid-auth-0.5/TODO.txt new file mode 100644 index 0000000..e69de29 diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/__init__.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/__init__.py new file mode 100644 index 0000000..b19fae1 --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/__init__.py @@ -0,0 +1,29 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2007 Simon Willison +# Copyright (C) 2008-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/admin.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/admin.py new file mode 100644 index 0000000..23b191a --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/admin.py @@ -0,0 +1,90 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2008-2013 Canonical Ltd. +# Copyright (C) 2010 Dave Walker +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from django.conf import settings +from django.contrib import admin +from django_openid_auth.models import Nonce, Association, UserOpenID +from django_openid_auth.store import DjangoOpenIDStore + + +class NonceAdmin(admin.ModelAdmin): + list_display = ('server_url', 'timestamp') + actions = ['cleanup_nonces'] + + def cleanup_nonces(self, request, queryset): + store = DjangoOpenIDStore() + count = store.cleanupNonces() + self.message_user(request, "%d expired nonces removed" % count) + cleanup_nonces.short_description = "Clean up expired nonces" + +admin.site.register(Nonce, NonceAdmin) + + +class AssociationAdmin(admin.ModelAdmin): + list_display = ('server_url', 'assoc_type') + list_filter = ('assoc_type',) + search_fields = ('server_url',) + actions = ['cleanup_associations'] + + def cleanup_associations(self, request, queryset): + store = DjangoOpenIDStore() + count = store.cleanupAssociations() + self.message_user(request, "%d expired associations removed" % count) + cleanup_associations.short_description = "Clean up expired associations" + +admin.site.register(Association, AssociationAdmin) + + +class UserOpenIDAdmin(admin.ModelAdmin): + raw_id_fields = ('user',) + list_display = ('user', 'claimed_id') + search_fields = ('claimed_id',) + +admin.site.register(UserOpenID, UserOpenIDAdmin) + + +# Support for allowing openid authentication for /admin (django.contrib.admin) +if getattr(settings, 'OPENID_USE_AS_ADMIN_LOGIN', False): + from django.http import HttpResponseRedirect + from django_openid_auth import views + + def _openid_login(self, request, error_message='', extra_context=None): + if request.user.is_authenticated(): + if not request.user.is_staff: + return views.default_render_failure( + request, "User %s does not have admin access." + % request.user.username) + assert error_message, "Unknown Error: %s" % error_message + else: + # Redirect to openid login path, + return HttpResponseRedirect( + settings.LOGIN_URL + "?next=" + request.get_full_path()) + + # Overide the standard admin login form. + admin.sites.AdminSite.login = _openid_login diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/auth.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/auth.py new file mode 100644 index 0000000..7395589 --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/auth.py @@ -0,0 +1,330 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2008-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +"""Glue between OpenID and django.contrib.auth.""" + +__metaclass__ = type + +from django.conf import settings +from django.contrib.auth.models import User, Group +from openid.consumer.consumer import SUCCESS +from openid.extensions import ax, sreg, pape + +from django_openid_auth import teams +from django_openid_auth.models import UserOpenID +from django_openid_auth.exceptions import ( + IdentityAlreadyClaimed, + DuplicateUsernameViolation, + MissingUsernameViolation, + MissingPhysicalMultiFactor, + RequiredAttributeNotReturned, +) + +class OpenIDBackend: + """A django.contrib.auth backend that authenticates the user based on + an OpenID response.""" + + supports_object_permissions = False + supports_anonymous_user = True + + def get_user(self, user_id): + try: + return User.objects.get(pk=user_id) + except User.DoesNotExist: + return None + + def authenticate(self, **kwargs): + """Authenticate the user based on an OpenID response.""" + # Require that the OpenID response be passed in as a keyword + # argument, to make sure we don't match the username/password + # calling conventions of authenticate. + + openid_response = kwargs.get('openid_response') + if openid_response is None: + return None + + if openid_response.status != SUCCESS: + return None + + user = None + try: + user_openid = UserOpenID.objects.get( + claimed_id__exact=openid_response.identity_url) + except UserOpenID.DoesNotExist: + if getattr(settings, 'OPENID_CREATE_USERS', False): + user = self.create_user_from_openid(openid_response) + else: + user = user_openid.user + + if user is None: + return None + + if getattr(settings, 'OPENID_UPDATE_DETAILS_FROM_SREG', False): + details = self._extract_user_details(openid_response) + self.update_user_details(user, details, openid_response) + + if getattr(settings, 'OPENID_PHYSICAL_MULTIFACTOR_REQUIRED', False): + pape_response = pape.Response.fromSuccessResponse(openid_response) + if pape_response is None or \ + pape.AUTH_MULTI_FACTOR_PHYSICAL not in pape_response.auth_policies: + raise MissingPhysicalMultiFactor() + + teams_response = teams.TeamsResponse.fromSuccessResponse( + openid_response) + if teams_response: + self.update_groups_from_teams(user, teams_response) + self.update_staff_status_from_teams(user, teams_response) + + return user + + def _extract_user_details(self, openid_response): + email = fullname = first_name = last_name = nickname = None + sreg_response = sreg.SRegResponse.fromSuccessResponse(openid_response) + if sreg_response: + email = sreg_response.get('email') + fullname = sreg_response.get('fullname') + nickname = sreg_response.get('nickname') + # If any attributes are provided via Attribute Exchange, use + # them in preference. + fetch_response = ax.FetchResponse.fromSuccessResponse(openid_response) + if fetch_response: + # The myOpenID provider advertises AX support, but uses + # attribute names from an obsolete draft of the + # specification. We check for them first so the common + # names take precedence. + email = fetch_response.getSingle( + 'http://schema.openid.net/contact/email', email) + fullname = fetch_response.getSingle( + 'http://schema.openid.net/namePerson', fullname) + nickname = fetch_response.getSingle( + 'http://schema.openid.net/namePerson/friendly', nickname) + + email = fetch_response.getSingle( + 'http://axschema.org/contact/email', email) + fullname = fetch_response.getSingle( + 'http://axschema.org/namePerson', fullname) + first_name = fetch_response.getSingle( + 'http://axschema.org/namePerson/first', first_name) + last_name = fetch_response.getSingle( + 'http://axschema.org/namePerson/last', last_name) + nickname = fetch_response.getSingle( + 'http://axschema.org/namePerson/friendly', nickname) + + if fullname and not (first_name or last_name): + # Django wants to store first and last names separately, + # so we do our best to split the full name. + fullname = fullname.strip() + split_names = fullname.rsplit(None, 1) + if len(split_names) == 2: + first_name, last_name = split_names + else: + first_name = u'' + last_name = fullname + + return dict(email=email, nickname=nickname, + first_name=first_name, last_name=last_name) + + def _get_preferred_username(self, nickname, email): + if nickname: + return nickname + if email and getattr(settings, 'OPENID_USE_EMAIL_FOR_USERNAME', + False): + suggestion = ''.join([x for x in email if x.isalnum()]) + if suggestion: + return suggestion + return 'openiduser' + + def _get_available_username(self, nickname, identity_url): + # If we're being strict about usernames, throw an error if we didn't + # get one back from the provider + if getattr(settings, 'OPENID_STRICT_USERNAMES', False): + if nickname is None or nickname == '': + raise MissingUsernameViolation() + + # If we don't have a nickname, and we're not being strict, use a default + nickname = nickname or 'openiduser' + + # See if we already have this nickname assigned to a username + try: + user = User.objects.get(username__exact=nickname) + except User.DoesNotExist: + # No conflict, we can use this nickname + return nickname + + # Check if we already have nickname+i for this identity_url + try: + user_openid = UserOpenID.objects.get( + claimed_id__exact=identity_url, + user__username__startswith=nickname) + # No exception means we have an existing user for this identity + # that starts with this nickname. + + # If they are an exact match, the user already exists and hasn't + # changed their username, so continue to use it + if nickname == user_openid.user.username: + return nickname + + # It is possible we've had to assign them to nickname+i already. + oid_username = user_openid.user.username + if len(oid_username) > len(nickname): + try: + # check that it ends with a number + int(oid_username[len(nickname):]) + return oid_username + except ValueError: + # username starts with nickname, but isn't nickname+# + pass + except UserOpenID.DoesNotExist: + # No user associated with this identity_url + pass + + + if getattr(settings, 'OPENID_STRICT_USERNAMES', False): + if User.objects.filter(username__exact=nickname).count() > 0: + raise DuplicateUsernameViolation( + "The username (%s) with which you tried to log in is " + "already in use for a different account." % nickname) + + # Pick a username for the user based on their nickname, + # checking for conflicts. Start with number of existing users who's + # username starts with this nickname to avoid having to iterate over + # all of the existing ones. + i = User.objects.filter(username__startswith=nickname).count() + 1 + while True: + username = nickname + if i > 1: + username += str(i) + try: + user = User.objects.get(username__exact=username) + except User.DoesNotExist: + break + i += 1 + return username + + def create_user_from_openid(self, openid_response): + details = self._extract_user_details(openid_response) + required_attrs = getattr(settings, 'OPENID_SREG_REQUIRED_FIELDS', []) + if getattr(settings, 'OPENID_STRICT_USERNAMES', False): + required_attrs.append('nickname') + + for required_attr in required_attrs: + if required_attr not in details or not details[required_attr]: + raise RequiredAttributeNotReturned( + "An attribute required for logging in was not " + "returned ({0}).".format(required_attr)) + + nickname = self._get_preferred_username(details['nickname'], + details['email']) + email = details['email'] or '' + + username = self._get_available_username(nickname, + openid_response.identity_url) + + user = User.objects.create_user(username, email, password=None) + self.associate_openid(user, openid_response) + self.update_user_details(user, details, openid_response) + + return user + + def associate_openid(self, user, openid_response): + """Associate an OpenID with a user account.""" + # Check to see if this OpenID has already been claimed. + try: + user_openid = UserOpenID.objects.get( + claimed_id__exact=openid_response.identity_url) + except UserOpenID.DoesNotExist: + user_openid = UserOpenID( + user=user, + claimed_id=openid_response.identity_url, + display_id=openid_response.endpoint.getDisplayIdentifier()) + user_openid.save() + else: + if user_openid.user != user: + raise IdentityAlreadyClaimed( + "The identity %s has already been claimed" + % openid_response.identity_url) + + return user_openid + + def update_user_details(self, user, details, openid_response): + updated = False + if details['first_name']: + user.first_name = details['first_name'][:30] + updated = True + if details['last_name']: + user.last_name = details['last_name'][:30] + updated = True + if details['email']: + user.email = details['email'] + updated = True + if getattr(settings, 'OPENID_FOLLOW_RENAMES', False): + user.username = self._get_available_username(details['nickname'], openid_response.identity_url) + updated = True + + if updated: + user.save() + + def update_groups_from_teams(self, user, teams_response): + teams_mapping_auto = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO', False) + teams_mapping_auto_blacklist = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST', []) + teams_mapping = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {}) + if teams_mapping_auto: + #ignore teams_mapping. use all django-groups + teams_mapping = dict() + all_groups = Group.objects.exclude(name__in=teams_mapping_auto_blacklist) + for group in all_groups: + teams_mapping[group.name] = group.name + + if len(teams_mapping) == 0: + return + + current_groups = set(user.groups.filter( + name__in=teams_mapping.values())) + desired_groups = set(Group.objects.filter( + name__in=[teams_mapping[lp_team] + for lp_team in teams_response.is_member + if lp_team in teams_mapping])) + for group in current_groups - desired_groups: + user.groups.remove(group) + for group in desired_groups - current_groups: + user.groups.add(group) + + def update_staff_status_from_teams(self, user, teams_response): + if not hasattr(settings, 'OPENID_LAUNCHPAD_STAFF_TEAMS'): + return + + staff_teams = getattr(settings, 'OPENID_LAUNCHPAD_STAFF_TEAMS', []) + user.is_staff = False + + for lp_team in teams_response.is_member: + if lp_team in staff_teams: + user.is_staff = True + break + + user.save() + diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/exceptions.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/exceptions.py new file mode 100644 index 0000000..60da2dd --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/exceptions.py @@ -0,0 +1,68 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2008-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +"""Exception classes thrown by OpenID Authentication and Validation.""" + +class DjangoOpenIDException(Exception): + pass + +class RequiredAttributeNotReturned(DjangoOpenIDException): + pass + +class IdentityAlreadyClaimed(DjangoOpenIDException): + + def __init__(self, message=None): + if message is None: + self.message = "Another user already exists for your selected OpenID" + else: + self.message = message + +class DuplicateUsernameViolation(DjangoOpenIDException): + + def __init__(self, message=None): + if message is None: + self.message = "Your desired username is already being used." + else: + self.message = message + +class MissingUsernameViolation(DjangoOpenIDException): + + def __init__(self, message=None): + if message is None: + self.message = "No nickname given for your account." + else: + self.message = message + +class MissingPhysicalMultiFactor(DjangoOpenIDException): + + def __init__(self, message=None): + if message is None: + self.message = "Login requires physical multi-factor authentication." + else: + self.message = message + diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/forms.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/forms.py new file mode 100644 index 0000000..7a2538f --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/forms.py @@ -0,0 +1,87 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2007 Simon Willison +# Copyright (C) 2008-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from django import forms +from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.forms import UserChangeForm +from django.contrib.auth.models import Group +from django.utils.translation import ugettext as _ +from django.conf import settings + +from openid.yadis import xri + + +def teams_new_unicode(self): + """ + Replacement for Group.__unicode__() + Calls original method to chain results + """ + name = self.unicode_before_teams() + teams_mapping = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {}) + group_teams = [t for t in teams_mapping if teams_mapping[t] == self.name] + if len(group_teams) > 0: + return "%s -> %s" % (name, ", ".join(group_teams)) + else: + return name +Group.unicode_before_teams = Group.__unicode__ +Group.__unicode__ = teams_new_unicode + + +class UserChangeFormWithTeamRestriction(UserChangeForm): + """ + Extends UserChangeForm to add teams awareness to the user admin form + """ + def clean_groups(self): + data = self.cleaned_data['groups'] + teams_mapping = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {}) + known_teams = teams_mapping.values() + user_groups = self.instance.groups.all() + for group in data: + if group.name in known_teams and group not in user_groups: + raise forms.ValidationError("""The group %s is mapped to an + external team. You cannot assign it manually.""" % group.name) + return data +UserAdmin.form = UserChangeFormWithTeamRestriction + + +class OpenIDLoginForm(forms.Form): + openid_identifier = forms.CharField( + max_length=255, + widget=forms.TextInput(attrs={'class': 'required openid'})) + + def clean_openid_identifier(self): + if 'openid_identifier' in self.cleaned_data: + openid_identifier = self.cleaned_data['openid_identifier'] + if xri.identifierScheme(openid_identifier) == 'XRI' and getattr( + settings, 'OPENID_DISALLOW_INAMES', False + ): + raise forms.ValidationError(_('i-names are not supported')) + return self.cleaned_data['openid_identifier'] + + diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/management/__init__.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/management/__init__.py new file mode 100644 index 0000000..ade3ebe --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/management/__init__.py @@ -0,0 +1,27 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2009-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/management/commands/__init__.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/management/commands/__init__.py new file mode 100644 index 0000000..ade3ebe --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/management/commands/__init__.py @@ -0,0 +1,27 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2009-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/management/commands/openid_cleanup.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/management/commands/openid_cleanup.py new file mode 100644 index 0000000..a5d3304 --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/management/commands/openid_cleanup.py @@ -0,0 +1,39 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2009-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from django.core.management.base import NoArgsCommand + +from django_openid_auth.store import DjangoOpenIDStore + + +class Command(NoArgsCommand): + help = 'Clean up stale OpenID associations and nonces' + + def handle_noargs(self, **options): + store = DjangoOpenIDStore() + store.cleanup() diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/models.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/models.py new file mode 100644 index 0000000..064e77a --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/models.py @@ -0,0 +1,58 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2007 Simon Willison +# Copyright (C) 2008-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from django.contrib.auth.models import User +from django.db import models + + +class Nonce(models.Model): + server_url = models.CharField(max_length=2047) + timestamp = models.IntegerField() + salt = models.CharField(max_length=40) + + def __unicode__(self): + return u"Nonce: %s, %s" % (self.server_url, self.salt) + + +class Association(models.Model): + server_url = models.TextField(max_length=2047) + handle = models.CharField(max_length=255) + secret = models.TextField(max_length=255) # Stored base64 encoded + issued = models.IntegerField() + lifetime = models.IntegerField() + assoc_type = models.TextField(max_length=64) + + def __unicode__(self): + return u"Association: %s, %s" % (self.server_url, self.handle) + + +class UserOpenID(models.Model): + user = models.ForeignKey(User) + claimed_id = models.TextField(max_length=2047, unique=True) + display_id = models.TextField(max_length=2047) diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/signals.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/signals.py new file mode 100644 index 0000000..6f3a74b --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/signals.py @@ -0,0 +1,34 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2007 Simon Willison +# Copyright (C) 2008-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import django.dispatch + + +openid_login_complete = django.dispatch.Signal(providing_args=[ + 'request', 'openid_response']) diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/store.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/store.py new file mode 100644 index 0000000..b804c6f --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/store.py @@ -0,0 +1,131 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2007 Simon Willison +# Copyright (C) 2008-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import base64 +import time + +from openid.association import Association as OIDAssociation +from openid.store.interface import OpenIDStore +from openid.store.nonce import SKEW + +from django_openid_auth.models import Association, Nonce + + +class DjangoOpenIDStore(OpenIDStore): + def __init__(self): + self.max_nonce_age = 6 * 60 * 60 # Six hours + + def storeAssociation(self, server_url, association): + try: + assoc = Association.objects.get( + server_url=server_url, handle=association.handle) + except Association.DoesNotExist: + assoc = Association( + server_url=server_url, + handle=association.handle, + secret=base64.encodestring(association.secret), + issued=association.issued, + lifetime=association.lifetime, + assoc_type=association.assoc_type) + else: + assoc.secret = base64.encodestring(association.secret) + assoc.issued = association.issued + assoc.lifetime = association.lifetime + assoc.assoc_type = association.assoc_type + assoc.save() + + def getAssociation(self, server_url, handle=None): + assocs = [] + if handle is not None: + assocs = Association.objects.filter( + server_url=server_url, handle=handle) + else: + assocs = Association.objects.filter(server_url=server_url) + associations = [] + expired = [] + for assoc in assocs: + association = OIDAssociation( + assoc.handle, base64.decodestring(assoc.secret), assoc.issued, + assoc.lifetime, assoc.assoc_type + ) + if association.getExpiresIn() == 0: + expired.append(assoc) + else: + associations.append((association.issued, association)) + for assoc in expired: + assoc.delete() + if not associations: + return None + associations.sort() + return associations[-1][1] + + def removeAssociation(self, server_url, handle): + assocs = list(Association.objects.filter( + server_url=server_url, handle=handle)) + assocs_exist = len(assocs) > 0 + for assoc in assocs: + assoc.delete() + return assocs_exist + + def useNonce(self, server_url, timestamp, salt): + if abs(timestamp - time.time()) > SKEW: + return False + + try: + ononce = Nonce.objects.get( + server_url__exact=server_url, + timestamp__exact=timestamp, + salt__exact=salt) + except Nonce.DoesNotExist: + ononce = Nonce( + server_url=server_url, + timestamp=timestamp, + salt=salt) + ononce.save() + return True + + return False + + def cleanupNonces(self, _now=None): + if _now is None: + _now = int(time.time()) + expired = Nonce.objects.filter(timestamp__lt=_now - SKEW) + count = expired.count() + if count: + expired.delete() + return count + + def cleanupAssociations(self): + now = int(time.time()) + expired = Association.objects.extra( + where=['issued + lifetime < %d' % now]) + count = expired.count() + if count: + expired.delete() + return count diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/teams.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/teams.py new file mode 100644 index 0000000..a11a4f1 --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/teams.py @@ -0,0 +1,411 @@ +# Launchpad OpenID Teams Extension support for python-openid +# +# Copyright (C) 2008-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +"""Team membership support for Launchpad. + +The primary form of communication between the RP and Launchpad is an +OpenID authentication request. Our solution is to piggyback a team +membership test onto this interaction. + +As part of an OpenID authentication request, the RP includes the +following fields: + + openid.ns.lp: + An OpenID 2.0 namespace URI for the extension. It is not strictly + required for 1.1 requests, but including it is good for forward + compatibility. + + It must be set to: http://ns.launchpad.net/2007/openid-teams + + openid.lp.query_membership: + A comma separated list of Launchpad team names that the RP is + interested in. + +As part of the positive assertion OpenID response, the following field +will be provided: + + openid.ns.lp: + (as above) + + openid.lp.is_member: + A comma separated list of teams that the user is actually a member + of. The list may be limited to those teams mentioned in the + request. + + This field must be included in the response signature in order to + be considered valid (as the response is bounced through the user's + web browser, an unsigned value could be modified). + +@since: 2.1.1 +""" + +from openid.message import registerNamespaceAlias, \ + NamespaceAliasRegistrationError +from openid.extension import Extension +from openid import oidutil + +try: + basestring #pylint:disable-msg=W0104 +except NameError: + # For Python 2.2 + basestring = (str, unicode) #pylint:disable-msg=W0622 + +__all__ = [ + 'TeamsRequest', + 'TeamsResponse', + 'ns_uri', + 'supportsTeams', + ] + +ns_uri = 'http://ns.launchpad.net/2007/openid-teams' + +try: + registerNamespaceAlias(ns_uri, 'lp') +except NamespaceAliasRegistrationError, e: + oidutil.log('registerNamespaceAlias(%r, %r) failed: %s' % (ns_uri, + 'lp', str(e),)) + +def supportsTeams(endpoint): + """Does the given endpoint advertise support for Launchpad Teams? + + @param endpoint: The endpoint object as returned by OpenID discovery + @type endpoint: openid.consumer.discover.OpenIDEndpoint + + @returns: Whether an lp type was advertised by the endpoint + @rtype: bool + """ + return endpoint.usesExtension(ns_uri) + +class TeamsNamespaceError(ValueError): + """The Launchpad teams namespace was not found and could not + be created using the expected name (there's another extension + using the name 'lp') + + This is not I{illegal}, for OpenID 2, although it probably + indicates a problem, since it's not expected that other extensions + will re-use the alias that is in use for OpenID 1. + + If this is an OpenID 1 request, then there is no recourse. This + should not happen unless some code has modified the namespaces for + the message that is being processed. + """ + +def getTeamsNS(message): + """Extract the Launchpad teams namespace URI from the given + OpenID message. + + @param message: The OpenID message from which to parse Launchpad + teams. This may be a request or response message. + @type message: C{L{openid.message.Message}} + + @returns: the lp namespace URI for the supplied message. The + message may be modified to define a Launchpad teams + namespace. + @rtype: C{str} + + @raise ValueError: when using OpenID 1 if the message defines + the 'lp' alias to be something other than a Launchpad + teams type. + """ + # See if there exists an alias for the Launchpad teams type. + alias = message.namespaces.getAlias(ns_uri) + if alias is None: + # There is no alias, so try to add one. (OpenID version 1) + try: + message.namespaces.addAlias(ns_uri, 'lp') + except KeyError, why: + # An alias for the string 'lp' already exists, but it's + # defined for something other than Launchpad teams + raise TeamsNamespaceError(why[0]) + + # we know that ns_uri defined, because it's defined in the + # else clause of the loop as well, so disable the warning + return ns_uri #pylint:disable-msg=W0631 + +class TeamsRequest(Extension): + """An object to hold the state of a Launchpad teams request. + + @ivar query_membership: A comma separated list of Launchpad team + names that the RP is interested in. + @type required: [str] + + @group Consumer: requestField, requestTeams, getExtensionArgs, addToOpenIDRequest + @group Server: fromOpenIDRequest, parseExtensionArgs + """ + + ns_alias = 'lp' + + def __init__(self, query_membership=None, lp_ns_uri=ns_uri): + """Initialize an empty Launchpad teams request""" + Extension.__init__(self) + self.query_membership = [] + self.ns_uri = lp_ns_uri + + if query_membership: + self.requestTeams(query_membership) + + # Assign getTeamsNS to a static method so that it can be + # overridden for testing. + _getTeamsNS = staticmethod(getTeamsNS) + + def fromOpenIDRequest(cls, request): + """Create a Launchpad teams request that contains the + fields that were requested in the OpenID request with the + given arguments + + @param request: The OpenID request + @type request: openid.server.CheckIDRequest + + @returns: The newly created Launchpad teams request + @rtype: C{L{TeamsRequest}} + """ + self = cls() + + # Since we're going to mess with namespace URI mapping, don't + # mutate the object that was passed in. + message = request.message.copy() + + self.ns_uri = self._getTeamsNS(message) + args = message.getArgs(self.ns_uri) + self.parseExtensionArgs(args) + + return self + + fromOpenIDRequest = classmethod(fromOpenIDRequest) + + def parseExtensionArgs(self, args, strict=False): + """Parse the unqualified Launchpad teams request + parameters and add them to this object. + + This method is essentially the inverse of + C{L{getExtensionArgs}}. This method restores the serialized + Launchpad teams request fields. + + If you are extracting arguments from a standard OpenID + checkid_* request, you probably want to use C{L{fromOpenIDRequest}}, + which will extract the lp namespace and arguments from the + OpenID request. This method is intended for cases where the + OpenID server needs more control over how the arguments are + parsed than that method provides. + + >>> args = message.getArgs(ns_uri) + >>> request.parseExtensionArgs(args) + + @param args: The unqualified Launchpad teams arguments + @type args: {str:str} + + @param strict: Whether requests with fields that are not + defined in the Launchpad teams specification should be + tolerated (and ignored) + @type strict: bool + + @returns: None; updates this object + """ + items = args.get('query_membership') + if items: + for team_name in items.split(','): + try: + self.requestTeam(team_name, strict) + except ValueError: + if strict: + raise + + def allRequestedTeams(self): + """A list of all of the Launchpad teams that were + requested. + + @rtype: [str] + """ + return self.query_membership + + def wereTeamsRequested(self): + """Have any Launchpad teams been requested? + + @rtype: bool + """ + return bool(self.allRequestedTeams()) + + def __contains__(self, team_name): + """Was this team in the request?""" + return team_name in self.query_membership + + def requestTeam(self, team_name, strict=False): + """Request the specified team from the OpenID user + + @param team_name: the unqualified Launchpad team name + @type team_name: str + + @param strict: whether to raise an exception when a team is + added to a request more than once + + @raise ValueError: when strict is set and the team was + requested more than once + """ + if strict: + if team_name in self.query_membership: + raise ValueError('That team has already been requested') + else: + if team_name in self.query_membership: + return + + self.query_membership.append(team_name) + + def requestTeams(self, query_membership, strict=False): + """Add the given list of teams to the request + + @param query_membership: The Launchpad teams request + @type query_membership: [str] + + @raise ValueError: when a team requested is not a string + or strict is set and a team was requested more than once + """ + if isinstance(query_membership, basestring): + raise TypeError('Teams should be passed as a list of ' + 'strings (not %r)' % (type(query_membership),)) + + for team_name in query_membership: + self.requestTeam(team_name, strict=strict) + + def getExtensionArgs(self): + """Get a dictionary of unqualified Launchpad teams + arguments representing this request. + + This method is essentially the inverse of + C{L{parseExtensionArgs}}. This method serializes the Launchpad + teams request fields. + + @rtype: {str:str} + """ + args = {} + + if self.query_membership: + args['query_membership'] = ','.join(self.query_membership) + + return args + +class TeamsResponse(Extension): + """Represents the data returned in a Launchpad teams response + inside of an OpenID C{id_res} response. This object will be + created by the OpenID server, added to the C{id_res} response + object, and then extracted from the C{id_res} message by the + Consumer. + + @ivar data: The Launchpad teams data, an array. + + @ivar ns_uri: The URI under which the Launchpad teams data was + stored in the response message. + + @group Server: extractResponse + @group Consumer: fromSuccessResponse + @group Read-only dictionary interface: keys, iterkeys, items, iteritems, + __iter__, get, __getitem__, keys, has_key + """ + + ns_alias = 'lp' + + def __init__(self, is_member=None, lp_ns_uri=ns_uri): + Extension.__init__(self) + if is_member is None: + self.is_member = [] + else: + self.is_member = is_member + + self.ns_uri = lp_ns_uri + + def addTeam(self, team_name): + if team_name not in self.is_member: + self.is_member.append(team_name) + + def extractResponse(cls, request, is_member_str): + """Take a C{L{TeamsRequest}} and a list of Launchpad + team values and create a C{L{TeamsResponse}} + object containing that data. + + @param request: The Launchpad teams request object + @type request: TeamsRequest + + @param is_member: The Launchpad teams data for this + response, as a list of strings. + @type is_member: {str:str} + + @returns: a Launchpad teams response object + @rtype: TeamsResponse + """ + self = cls() + self.ns_uri = request.ns_uri + self.is_member = is_member_str.split(',') + return self + + extractResponse = classmethod(extractResponse) + + # Assign getTeamsNS to a static method so that it can be + # overridden for testing + _getTeamsNS = staticmethod(getTeamsNS) + + def fromSuccessResponse(cls, success_response, signed_only=True): + """Create a C{L{TeamsResponse}} object from a successful OpenID + library response + (C{L{openid.consumer.consumer.SuccessResponse}}) response + message + + @param success_response: A SuccessResponse from consumer.complete() + @type success_response: C{L{openid.consumer.consumer.SuccessResponse}} + + @param signed_only: Whether to process only data that was + signed in the id_res message from the server. + @type signed_only: bool + + @rtype: TeamsResponse + @returns: A Launchpad teams response containing the data + that was supplied with the C{id_res} response. + """ + self = cls() + self.ns_uri = self._getTeamsNS(success_response.message) + if signed_only: + args = success_response.getSignedNS(self.ns_uri) + else: + args = success_response.message.getArgs(self.ns_uri) + + if "is_member" in args: + is_member_str = args["is_member"] + self.is_member = is_member_str.split(',') + #self.is_member = args["is_member"] + + return self + + fromSuccessResponse = classmethod(fromSuccessResponse) + + def getExtensionArgs(self): + """Get the fields to put in the Launchpad teams namespace + when adding them to an id_res message. + + @see: openid.extension + """ + ns_args = {'is_member': ','.join(self.is_member),} + return ns_args + diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/templates/openid/failure.html b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/templates/openid/failure.html new file mode 100644 index 0000000..3771051 --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/templates/openid/failure.html @@ -0,0 +1,11 @@ + + + + OpenID failed + + +

OpenID failed

+

{{ message|escape }}

+ + diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/templates/openid/login.html b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/templates/openid/login.html new file mode 100644 index 0000000..1d682df --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/templates/openid/login.html @@ -0,0 +1,43 @@ +{% load i18n %} + + + +Sign in with your OpenID + + + +

Sign in with your OpenID

+{% if form.errors %} +

{% trans "Please correct errors below:" %}
+ {% if form.openid_identifier.errors %} + {{ form.openid_identifier.errors|join:", " }} + {% endif %} + {% if form.next.errors %} + {{ form.next.errors|join:", " }} + {% endif %} +

+{% endif %} +
+ {% csrf_token %} +
+ {% trans "Sign In Using Your OpenID" %} +
+
+ {{ form.openid_identifier }} +
+
+ +
+ {% if next %} + + {% endif %} +
+
+ + diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/__init__.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/__init__.py new file mode 100644 index 0000000..f76af6f --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/__init__.py @@ -0,0 +1,41 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2009-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import unittest +from test_views import * +from test_store import * +from test_auth import * +from test_admin import * + + +def suite(): + suite = unittest.TestSuite() + for name in ['test_auth', 'test_store', 'test_views', 'test_admin']: + mod = __import__('%s.%s' % (__name__, name), {}, {}, ['suite']) + suite.addTest(mod.suite()) + return suite diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_admin.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_admin.py new file mode 100644 index 0000000..a3e2adb --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_admin.py @@ -0,0 +1,88 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2009-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +""" +Tests for the django_openid_auth Admin login form replacement. +""" + +import os +import unittest + +from django.conf import settings +from django.contrib.auth.models import User, AnonymousUser + +settings.OPENID_USE_AS_ADMIN_LOGIN = True +from django_openid_auth import admin + +from django.test import TestCase + + +def create_user(is_staff=False, authenticated=True): + """ + Create and return a user, either the AnonymousUser or a normal Django user, + setting the is_staff attribute if appropriate. + """ + if not authenticated: + return AnonymousUser() + else: + user = User( + username=u'testing', email='testing@example.com', + is_staff=is_staff) + user.set_password(u'test') + user.save() + + +class SiteAdminTests(TestCase): + """ + TestCase for accessing /admin/ when the django_openid_auth form replacement + is in use. + """ + + def test_admin_site_with_openid_login_authenticated_non_staff(self): + """ + If the request has an authenticated user, who is not flagged as a + staff member, then they get a failure response. + """ + create_user() + self.client.login(username='testing', password='test') + response = self.client.get('/admin/') + self.assertTrue('User testing does not have admin access.' in + response.content, 'Missing error message in response') + + def test_admin_site_with_openid_login_non_authenticated_user(self): + """ + Unauthenticated users accessing the admin page should be directed to + the OpenID login url. + """ + response = self.client.get('/admin/') + self.assertEqual(302, response.status_code) + self.assertEqual('http://testserver/openid/login/?next=/admin/', + response['Location']) + + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_auth.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_auth.py new file mode 100644 index 0000000..ae0d848 --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_auth.py @@ -0,0 +1,185 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2010-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import unittest + +from django.conf import settings +from django.contrib.auth.models import User +from django.test import TestCase + +from django_openid_auth.auth import OpenIDBackend +from openid.consumer.consumer import SuccessResponse +from openid.consumer.discover import OpenIDServiceEndpoint +from openid.message import Message, OPENID2_NS + + +SREG_NS = "http://openid.net/sreg/1.0" +AX_NS = "http://openid.net/srv/ax/1.0" + +class OpenIDBackendTests(TestCase): + + def setUp(self): + super(OpenIDBackendTests, self).setUp() + self.backend = OpenIDBackend() + self.old_openid_use_email_for_username = getattr(settings, + 'OPENID_USE_EMAIL_FOR_USERNAME', False) + + def tearDown(self): + settings.OPENID_USE_EMAIL_FOR_USERNAME = \ + self.old_openid_use_email_for_username + + def test_extract_user_details_sreg(self): + endpoint = OpenIDServiceEndpoint() + message = Message(OPENID2_NS) + message.setArg(SREG_NS, "nickname", "someuser") + message.setArg(SREG_NS, "fullname", "Some User") + message.setArg(SREG_NS, "email", "foo@example.com") + response = SuccessResponse( + endpoint, message, signed_fields=message.toPostArgs().keys()) + + data = self.backend._extract_user_details(response) + self.assertEqual(data, {"nickname": "someuser", + "first_name": "Some", + "last_name": "User", + "email": "foo@example.com"}) + + def make_response_ax(self, schema="http://axschema.org/", + fullname="Some User", nickname="someuser", email="foo@example.com", + first=None, last=None): + endpoint = OpenIDServiceEndpoint() + message = Message(OPENID2_NS) + attributes = [ + ("nickname", schema + "namePerson/friendly", nickname), + ("fullname", schema + "namePerson", fullname), + ("email", schema + "contact/email", email), + ] + if first: + attributes.append( + ("first", "http://axschema.org/namePerson/first", first)) + if last: + attributes.append( + ("last", "http://axschema.org/namePerson/last", last)) + + message.setArg(AX_NS, "mode", "fetch_response") + for (alias, uri, value) in attributes: + message.setArg(AX_NS, "type.%s" % alias, uri) + message.setArg(AX_NS, "value.%s" % alias, value) + return SuccessResponse( + endpoint, message, signed_fields=message.toPostArgs().keys()) + + def test_extract_user_details_ax(self): + response = self.make_response_ax(fullname="Some User", + nickname="someuser", email="foo@example.com") + + data = self.backend._extract_user_details(response) + + self.assertEqual(data, {"nickname": "someuser", + "first_name": "Some", + "last_name": "User", + "email": "foo@example.com"}) + + def test_extract_user_details_ax_split_name(self): + # Include fullname too to show that the split data takes + # precedence. + response = self.make_response_ax( + fullname="Bad Data", first="Some", last="User") + + data = self.backend._extract_user_details(response) + + self.assertEqual(data, {"nickname": "someuser", + "first_name": "Some", + "last_name": "User", + "email": "foo@example.com"}) + + def test_extract_user_details_ax_broken_myopenid(self): + response = self.make_response_ax( + schema="http://schema.openid.net/", fullname="Some User", + nickname="someuser", email="foo@example.com") + + data = self.backend._extract_user_details(response) + + self.assertEqual(data, {"nickname": "someuser", + "first_name": "Some", + "last_name": "User", + "email": "foo@example.com"}) + + def test_update_user_details_long_names(self): + response = self.make_response_ax() + user = User.objects.create_user('someuser', 'someuser@example.com', + password=None) + data = dict(first_name=u"Some56789012345678901234567890123", + last_name=u"User56789012345678901234567890123", + email=u"someotheruser@example.com") + + self.backend.update_user_details(user, data, response) + + self.assertEqual("Some56789012345678901234567890", user.first_name) + self.assertEqual("User56789012345678901234567890", user.last_name) + + def test_extract_user_details_name_with_trailing_space(self): + response = self.make_response_ax(fullname="SomeUser ") + + data = self.backend._extract_user_details(response) + + self.assertEqual("", data['first_name']) + self.assertEqual("SomeUser", data['last_name']) + + def test_extract_user_details_name_with_thin_space(self): + response = self.make_response_ax(fullname=u"Some\u2009User") + + data = self.backend._extract_user_details(response) + + self.assertEqual("Some", data['first_name']) + self.assertEqual("User", data['last_name']) + + def test_preferred_username_email_munging(self): + settings.OPENID_USE_EMAIL_FOR_USERNAME = True + for nick, email, expected in [ + ('nickcomesfirst', 'foo@example.com', 'nickcomesfirst'), + ('', 'foo@example.com', 'fooexamplecom'), + ('noemail', '', 'noemail'), + ('', '@%.-', 'openiduser'), + ('', '', 'openiduser'), + (None, None, 'openiduser')]: + self.assertEqual(expected, + self.backend._get_preferred_username(nick, email)) + + def test_preferred_username_no_email_munging(self): + for nick, email, expected in [ + ('nickcomesfirst', 'foo@example.com', 'nickcomesfirst'), + ('', 'foo@example.com', 'openiduser'), + ('noemail', '', 'noemail'), + ('', '@%.-', 'openiduser'), + ('', '', 'openiduser'), + (None, None, 'openiduser')]: + self.assertEqual(expected, + self.backend._get_preferred_username(nick, email)) + + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_store.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_store.py new file mode 100644 index 0000000..7b2af93 --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_store.py @@ -0,0 +1,193 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2009-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import time +import unittest + +from django.test import TestCase +from openid.association import Association as OIDAssociation +from openid.store.nonce import SKEW + +from django_openid_auth.models import Association, Nonce +from django_openid_auth.store import DjangoOpenIDStore + + +class OpenIDStoreTests(TestCase): + + def setUp(self): + super(OpenIDStoreTests, self).setUp() + self.store = DjangoOpenIDStore() + + def test_storeAssociation(self): + assoc = OIDAssociation('handle', 'secret', 42, 600, 'HMAC-SHA1') + self.store.storeAssociation('server-url', assoc) + + dbassoc = Association.objects.get( + server_url='server-url', handle='handle') + self.assertEquals(dbassoc.server_url, 'server-url') + self.assertEquals(dbassoc.handle, 'handle') + self.assertEquals(dbassoc.secret, 'secret'.encode('base-64')) + self.assertEquals(dbassoc.issued, 42) + self.assertEquals(dbassoc.lifetime, 600) + self.assertEquals(dbassoc.assoc_type, 'HMAC-SHA1') + + def test_storeAssociation_update_existing(self): + assoc = OIDAssociation('handle', 'secret', 42, 600, 'HMAC-SHA1') + self.store.storeAssociation('server-url', assoc) + + # Now update the association with new information. + assoc = OIDAssociation('handle', 'secret2', 420, 900, 'HMAC-SHA256') + self.store.storeAssociation('server-url', assoc) + dbassoc = Association.objects.get( + server_url='server-url', handle='handle') + self.assertEqual(dbassoc.secret, 'secret2'.encode('base-64')) + self.assertEqual(dbassoc.issued, 420) + self.assertEqual(dbassoc.lifetime, 900) + self.assertEqual(dbassoc.assoc_type, 'HMAC-SHA256') + + def test_getAssociation(self): + timestamp = int(time.time()) + self.store.storeAssociation( + 'server-url', OIDAssociation('handle', 'secret', timestamp, 600, + 'HMAC-SHA1')) + assoc = self.store.getAssociation('server-url', 'handle') + self.assertTrue(isinstance(assoc, OIDAssociation)) + + self.assertEquals(assoc.handle, 'handle') + self.assertEquals(assoc.secret, 'secret') + self.assertEquals(assoc.issued, timestamp) + self.assertEquals(assoc.lifetime, 600) + self.assertEquals(assoc.assoc_type, 'HMAC-SHA1') + + def test_getAssociation_unknown(self): + assoc = self.store.getAssociation('server-url', 'unknown') + self.assertEquals(assoc, None) + + def test_getAssociation_expired(self): + lifetime = 600 + timestamp = int(time.time()) - 2 * lifetime + self.store.storeAssociation( + 'server-url', OIDAssociation('handle', 'secret', timestamp, + lifetime, 'HMAC-SHA1')) + + # The association is not returned, and is removed from the database. + assoc = self.store.getAssociation('server-url', 'handle') + self.assertEquals(assoc, None) + self.assertRaises(Association.DoesNotExist, Association.objects.get, + server_url='server-url', handle='handle') + + def test_getAssociation_no_handle(self): + timestamp = int(time.time()) + + self.store.storeAssociation( + 'server-url', OIDAssociation('handle1', 'secret', timestamp + 1, + 600, 'HMAC-SHA1')) + self.store.storeAssociation( + 'server-url', OIDAssociation('handle2', 'secret', timestamp, + 600, 'HMAC-SHA1')) + + # The newest handle is returned. + assoc = self.store.getAssociation('server-url', None) + self.assertNotEquals(assoc, None) + self.assertEquals(assoc.handle, 'handle1') + self.assertEquals(assoc.issued, timestamp + 1) + + def test_removeAssociation(self): + timestamp = int(time.time()) + self.store.storeAssociation( + 'server-url', OIDAssociation('handle', 'secret', timestamp, 600, + 'HMAC-SHA1')) + self.assertEquals( + self.store.removeAssociation('server-url', 'handle'), True) + self.assertEquals( + self.store.getAssociation('server-url', 'handle'), None) + + def test_removeAssociation_unknown(self): + self.assertEquals( + self.store.removeAssociation('server-url', 'unknown'), False) + + def test_useNonce(self): + timestamp = time.time() + # The nonce can only be used once. + self.assertEqual( + self.store.useNonce('server-url', timestamp, 'salt'), True) + self.assertEqual( + self.store.useNonce('server-url', timestamp, 'salt'), False) + self.assertEqual( + self.store.useNonce('server-url', timestamp, 'salt'), False) + + def test_useNonce_expired(self): + timestamp = time.time() - 2 * SKEW + self.assertEqual( + self.store.useNonce('server-url', timestamp, 'salt'), False) + + def test_useNonce_future(self): + timestamp = time.time() + 2 * SKEW + self.assertEqual( + self.store.useNonce('server-url', timestamp, 'salt'), False) + + def test_cleanupNonces(self): + timestamp = time.time() + self.assertEqual( + self.store.useNonce('server1', timestamp, 'salt1'), True) + self.assertEqual( + self.store.useNonce('server2', timestamp, 'salt2'), True) + self.assertEqual( + self.store.useNonce('server3', timestamp, 'salt3'), True) + self.assertEqual(Nonce.objects.count(), 3) + + self.assertEqual( + self.store.cleanupNonces(_now=timestamp + 2 * SKEW), 3) + self.assertEqual(Nonce.objects.count(), 0) + + # The nonces have now been cleared: + self.assertEqual( + self.store.useNonce('server1', timestamp, 'salt1'), True) + self.assertEqual( + self.store.cleanupNonces(_now=timestamp + 2 * SKEW), 1) + self.assertEqual( + self.store.cleanupNonces(_now=timestamp + 2 * SKEW), 0) + + def test_cleanupAssociations(self): + timestamp = int(time.time()) - 100 + self.store.storeAssociation( + 'server-url', OIDAssociation('handle1', 'secret', timestamp, + 50, 'HMAC-SHA1')) + self.store.storeAssociation( + 'server-url', OIDAssociation('handle2', 'secret', timestamp, + 200, 'HMAC-SHA1')) + + self.assertEquals(self.store.cleanupAssociations(), 1) + + # The second (non-expired) association is left behind. + self.assertNotEqual(self.store.getAssociation('server-url', 'handle2'), + None) + + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_views.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_views.py new file mode 100644 index 0000000..ba17332 --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/test_views.py @@ -0,0 +1,1376 @@ +# -*- coding: utf-8 -*- +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2009-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import cgi +import unittest +from urllib import quote_plus + +from django.conf import settings +from django.contrib.auth.models import User, Group +from django.http import HttpRequest, HttpResponse +from django.test import TestCase +from openid.consumer.consumer import Consumer, SuccessResponse +from openid.consumer.discover import OpenIDServiceEndpoint +from openid.extensions import ax, sreg, pape +from openid.fetchers import ( + HTTPFetcher, HTTPFetchingError, HTTPResponse, setDefaultFetcher) +from openid.oidutil import importElementTree +from openid.server.server import BROWSER_REQUEST_MODES, ENCODE_URL, Server +from openid.store.memstore import MemoryStore +from openid.message import IDENTIFIER_SELECT + +from django_openid_auth import teams +from django_openid_auth.models import UserOpenID +from django_openid_auth.views import ( + sanitise_redirect_url, + make_consumer, +) +from django_openid_auth.signals import openid_login_complete +from django_openid_auth.store import DjangoOpenIDStore +from django_openid_auth.exceptions import ( + MissingUsernameViolation, + DuplicateUsernameViolation, + MissingPhysicalMultiFactor, + RequiredAttributeNotReturned, +) + +ET = importElementTree() + +class StubOpenIDProvider(HTTPFetcher): + + def __init__(self, base_url): + self.store = MemoryStore() + self.identity_url = base_url + 'identity' + self.localid_url = base_url + 'localid' + self.endpoint_url = base_url + 'endpoint' + self.server = Server(self.store, self.endpoint_url) + self.last_request = None + self.type_uris = ['http://specs.openid.net/auth/2.0/signon'] + + def fetch(self, url, body=None, headers=None): + if url == self.identity_url: + # Serve an XRDS document directly, pointing at our endpoint. + type_uris = ['%s' % uri for uri in self.type_uris] + return HTTPResponse( + url, 200, {'content-type': 'application/xrds+xml'}, """\ + + + + + %s + %s + %s + + + +""" % ('\n'.join(type_uris), self.endpoint_url, self.localid_url)) + elif url.startswith(self.endpoint_url): + # Gather query parameters + query = {} + if '?' in url: + query.update(cgi.parse_qsl(url.split('?', 1)[1])) + if body is not None: + query.update(cgi.parse_qsl(body)) + self.last_request = self.server.decodeRequest(query) + + # The browser based requests should not be handled through + # the fetcher interface. + assert self.last_request.mode not in BROWSER_REQUEST_MODES + + response = self.server.handleRequest(self.last_request) + webresponse = self.server.encodeResponse(response) + return HTTPResponse(url, webresponse.code, webresponse.headers, + webresponse.body) + else: + raise HTTPFetchingError('unknown URL %s' % url) + + def parseFormPost(self, content): + """Parse an HTML form post to create an OpenID request.""" + # Hack to make the javascript XML compliant ... + content = content.replace('i < elements.length', + 'i < elements.length') + tree = ET.XML(content) + form = tree.find('.//form') + assert form is not None, 'No form in document' + assert form.get('action') == self.endpoint_url, ( + 'Form posts to %s instead of %s' % (form.get('action'), + self.endpoint_url)) + query = {} + for input in form.findall('input'): + if input.get('type') != 'hidden': + continue + query[input.get('name').encode('UTF-8')] = \ + input.get('value').encode('UTF-8') + self.last_request = self.server.decodeRequest(query) + return self.last_request + + +class DummyDjangoRequest(object): + def __init__(self, request_path): + self.request_path = request_path + self.META = { + 'HTTP_HOST': "localhost", + 'SCRIPT_NAME': "http://localhost", + 'SERVER_PROTOCOL': "http", + } + self.POST = { + 'openid_identifier': "http://example.com/identity", + } + self.GET = {} + self.session = {} + + def get_full_path(self): + return self.META['SCRIPT_NAME'] + self.request_path + + def build_absolute_uri(self): + return self.META['SCRIPT_NAME'] + self.request_path + + def _combined_request(self): + request = {} + request.update(self.POST) + request.update(self.GET) + return request + REQUEST = property(_combined_request) + +class RelyingPartyTests(TestCase): + urls = 'django_openid_auth.tests.urls' + + def setUp(self): + super(RelyingPartyTests, self).setUp() + self.provider = StubOpenIDProvider('http://example.com/') + self.req = DummyDjangoRequest('http://localhost/') + self.endpoint = OpenIDServiceEndpoint() + self.endpoint.claimed_id = 'http://example.com/identity' + server_url = 'http://example.com/' + self.endpoint.server_url = server_url + self.consumer = make_consumer(self.req) + self.server = Server(DjangoOpenIDStore(), op_endpoint=server_url) + setDefaultFetcher(self.provider, wrap_exceptions=False) + + self.old_login_redirect_url = getattr(settings, 'LOGIN_REDIRECT_URL', '/accounts/profile/') + self.old_create_users = getattr(settings, 'OPENID_CREATE_USERS', False) + self.old_strict_usernames = getattr(settings, 'OPENID_STRICT_USERNAMES', False) + self.old_update_details = getattr(settings, 'OPENID_UPDATE_DETAILS_FROM_SREG', False) + self.old_sso_server_url = getattr(settings, 'OPENID_SSO_SERVER_URL', None) + self.old_teams_map = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {}) + self.old_use_as_admin_login = getattr(settings, 'OPENID_USE_AS_ADMIN_LOGIN', False) + self.old_follow_renames = getattr(settings, 'OPENID_FOLLOW_RENAMES', False) + self.old_physical_multifactor = getattr(settings, 'OPENID_PHYSICAL_MULTIFACTOR_REQUIRED', False) + self.old_login_render_failure = getattr(settings, 'OPENID_RENDER_FAILURE', None) + self.old_consumer_complete = Consumer.complete + self.old_openid_use_email_for_username = getattr(settings, + 'OPENID_USE_EMAIL_FOR_USERNAME', False) + + self.old_required_fields = getattr( + settings, 'OPENID_SREG_REQUIRED_FIELDS', []) + + settings.OPENID_CREATE_USERS = False + settings.OPENID_STRICT_USERNAMES = False + settings.OPENID_UPDATE_DETAILS_FROM_SREG = False + settings.OPENID_SSO_SERVER_URL = None + settings.OPENID_LAUNCHPAD_TEAMS_MAPPING = {} + settings.OPENID_USE_AS_ADMIN_LOGIN = False + settings.OPENID_FOLLOW_RENAMES = False + settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = False + settings.OPENID_SREG_REQUIRED_FIELDS = [] + settings.OPENID_USE_EMAIL_FOR_USERNAME = False + + def tearDown(self): + settings.LOGIN_REDIRECT_URL = self.old_login_redirect_url + settings.OPENID_CREATE_USERS = self.old_create_users + settings.OPENID_STRICT_USERNAMES = self.old_strict_usernames + settings.OPENID_UPDATE_DETAILS_FROM_SREG = self.old_update_details + settings.OPENID_SSO_SERVER_URL = self.old_sso_server_url + settings.OPENID_LAUNCHPAD_TEAMS_MAPPING = self.old_teams_map + settings.OPENID_USE_AS_ADMIN_LOGIN = self.old_use_as_admin_login + settings.OPENID_FOLLOW_RENAMES = self.old_follow_renames + settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = self.old_physical_multifactor + settings.OPENID_RENDER_FAILURE = self.old_login_render_failure + Consumer.complete = self.old_consumer_complete + settings.OPENID_SREG_REQUIRED_FIELDS = self.old_required_fields + settings.OPENID_USE_EMAIL_FOR_USERNAME = self.old_openid_use_email_for_username + + setDefaultFetcher(None) + super(RelyingPartyTests, self).tearDown() + + def complete(self, openid_response): + """Complete an OpenID authentication request.""" + # The server can generate either a redirect or a form post + # here. For simplicity, force generation of a redirect. + openid_response.whichEncoding = lambda: ENCODE_URL + webresponse = self.provider.server.encodeResponse(openid_response) + self.assertEquals(webresponse.code, 302) + redirect_to = webresponse.headers['location'] + self.assertTrue(redirect_to.startswith( + 'http://testserver/openid/complete/')) + return self.client.get('/openid/complete/', + dict(cgi.parse_qsl(redirect_to.split('?', 1)[1]))) + + def test_login(self): + user = User.objects.create_user('someuser', 'someone@example.com') + useropenid = UserOpenID( + user=user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + useropenid.save() + + # The login form is displayed: + response = self.client.get('/openid/login/') + self.assertTemplateUsed(response, 'openid/login.html') + + # Posting in an identity URL begins the authentication request: + response = self.client.post('/openid/login/', + {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'}) + self.assertContains(response, 'OpenID transaction in progress') + + openid_request = self.provider.parseFormPost(response.content) + self.assertEquals(openid_request.mode, 'checkid_setup') + self.assertTrue(openid_request.return_to.startswith( + 'http://testserver/openid/complete/')) + + # Complete the request. The user is redirected to the next URL. + openid_response = openid_request.answer(True) + response = self.complete(openid_response) + self.assertRedirects(response, 'http://testserver/getuser/') + + # And they are now logged in: + response = self.client.get('/getuser/') + self.assertEquals(response.content, 'someuser') + + def test_login_with_nonascii_return_to(self): + """Ensure non-ascii characters can be used for the 'next' arg.""" + response = self.client.post('/openid/login/', + {'openid_identifier': 'http://example.com/identity', + 'next': u'/files/ñandú.jpg'.encode('utf-8')}) + self.assertContains(response, 'OpenID transaction in progress') + + def test_login_no_next(self): + """Logins with no next parameter redirect to LOGIN_REDIRECT_URL.""" + user = User.objects.create_user('someuser', 'someone@example.com') + useropenid = UserOpenID( + user=user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + useropenid.save() + + settings.LOGIN_REDIRECT_URL = '/getuser/' + response = self.client.post('/openid/login/', + {'openid_identifier': 'http://example.com/identity'}) + self.assertContains(response, 'OpenID transaction in progress') + + openid_request = self.provider.parseFormPost(response.content) + self.assertEquals(openid_request.mode, 'checkid_setup') + self.assertTrue(openid_request.return_to.startswith( + 'http://testserver/openid/complete/')) + + # Complete the request. The user is redirected to the next URL. + openid_response = openid_request.answer(True) + response = self.complete(openid_response) + self.assertRedirects( + response, 'http://testserver' + settings.LOGIN_REDIRECT_URL) + + def test_login_sso(self): + settings.OPENID_SSO_SERVER_URL = 'http://example.com/identity' + user = User.objects.create_user('someuser', 'someone@example.com') + useropenid = UserOpenID( + user=user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + useropenid.save() + + # Requesting the login form immediately begins an + # authentication request. + response = self.client.get('/openid/login/', {'next': '/getuser/'}) + self.assertEquals(response.status_code, 200) + self.assertContains(response, 'OpenID transaction in progress') + + openid_request = self.provider.parseFormPost(response.content) + self.assertEquals(openid_request.mode, 'checkid_setup') + self.assertTrue(openid_request.return_to.startswith( + 'http://testserver/openid/complete/')) + + # Complete the request. The user is redirected to the next URL. + openid_response = openid_request.answer(True) + response = self.complete(openid_response) + self.assertRedirects(response, 'http://testserver/getuser/') + + # And they are now logged in: + response = self.client.get('/getuser/') + self.assertEquals(response.content, 'someuser') + + def test_login_create_users(self): + settings.OPENID_CREATE_USERS = True + # Create a user with the same name as we'll pass back via sreg. + User.objects.create_user('someuser', 'someone@example.com') + + # Posting in an identity URL begins the authentication request: + response = self.client.post('/openid/login/', + {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'}) + self.assertContains(response, 'OpenID transaction in progress') + + # Complete the request, passing back some simple registration + # data. The user is redirected to the next URL. + openid_request = self.provider.parseFormPost(response.content) + sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request) + openid_response = openid_request.answer(True) + sreg_response = sreg.SRegResponse.extractResponse( + sreg_request, {'nickname': 'someuser', 'fullname': 'Some User', + 'email': 'foo@example.com'}) + openid_response.addExtension(sreg_response) + response = self.complete(openid_response) + self.assertRedirects(response, 'http://testserver/getuser/') + + # And they are now logged in as a new user (they haven't taken + # over the existing "someuser" user). + response = self.client.get('/getuser/') + self.assertEquals(response.content, 'someuser2') + + # Check the details of the new user. + user = User.objects.get(username='someuser2') + self.assertEquals(user.first_name, 'Some') + self.assertEquals(user.last_name, 'User') + self.assertEquals(user.email, 'foo@example.com') + + def _do_user_login(self, req_data, resp_data, use_sreg=True, use_pape=None): + openid_request = self._get_login_request(req_data) + openid_response = self._get_login_response(openid_request, resp_data, use_sreg, use_pape) + response = self.complete(openid_response) + self.assertRedirects(response, 'http://testserver/getuser/') + return response + + def _get_login_request(self, req_data): + # Posting in an identity URL begins the authentication request: + response = self.client.post('/openid/login/', req_data) + self.assertContains(response, 'OpenID transaction in progress') + + # Complete the request, passing back some simple registration + # data. The user is redirected to the next URL. + openid_request = self.provider.parseFormPost(response.content) + return openid_request + + def _get_login_response(self, openid_request, resp_data, use_sreg, use_pape): + openid_response = openid_request.answer(True) + + if use_sreg: + sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request) + sreg_response = sreg.SRegResponse.extractResponse( + sreg_request, resp_data) + openid_response.addExtension(sreg_response) + if use_pape is not None: + policies = [ + use_pape + ] + pape_response = pape.Response(auth_policies=policies) + openid_response.addExtension(pape_response) + return openid_response + + def parse_query_string(self, query_str): + query_items = map(tuple, + [item.split('=') for item in query_str.split('&')]) + query = dict(query_items) + return query + + def test_login_physical_multifactor_request(self): + settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = True + preferred_auth = pape.AUTH_MULTI_FACTOR_PHYSICAL + self.provider.type_uris.append(pape.ns_uri) + + openid_req = {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'} + response = self.client.post('/openid/login/', openid_req) + openid_request = self.provider.parseFormPost(response.content) + + request_auth = openid_request.message.getArg( + 'http://specs.openid.net/extensions/pape/1.0', + 'preferred_auth_policies', + ) + self.assertEqual(request_auth, preferred_auth) + + def test_login_physical_multifactor_response(self): + settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = True + preferred_auth = pape.AUTH_MULTI_FACTOR_PHYSICAL + self.provider.type_uris.append(pape.ns_uri) + + def mock_complete(this, request_args, return_to): + request = {'openid.mode': 'checkid_setup', + 'openid.trust_root': 'http://localhost/', + 'openid.return_to': 'http://localhost/', + 'openid.identity': IDENTIFIER_SELECT, + 'openid.ns.pape' : pape.ns_uri, + 'openid.pape.auth_policies': request_args.get('openid.pape.auth_policies', pape.AUTH_NONE), + } + openid_server = self.provider.server + orequest = openid_server.decodeRequest(request) + response = SuccessResponse( + self.endpoint, orequest.message, + signed_fields=['openid.pape.auth_policies',]) + return response + Consumer.complete = mock_complete + + user = User.objects.create_user('testuser', 'test@example.com') + useropenid = UserOpenID( + user=user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + useropenid.save() + + openid_req = {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'} + openid_resp = {'nickname': 'testuser', 'fullname': 'Openid User', + 'email': 'test@example.com'} + + response = self._do_user_login(openid_req, openid_resp, use_pape=pape.AUTH_MULTI_FACTOR_PHYSICAL) + + query = self.parse_query_string(response.request['QUERY_STRING']) + self.assertTrue('openid.pape.auth_policies' in query) + self.assertEqual(query['openid.pape.auth_policies'], + quote_plus(preferred_auth)) + + response = self.client.get('/getuser/') + self.assertEqual(response.content, 'testuser') + + + def test_login_physical_multifactor_not_provided(self): + settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = True + preferred_auth = pape.AUTH_MULTI_FACTOR_PHYSICAL + self.provider.type_uris.append(pape.ns_uri) + + def mock_complete(this, request_args, return_to): + request = {'openid.mode': 'checkid_setup', + 'openid.trust_root': 'http://localhost/', + 'openid.return_to': 'http://localhost/', + 'openid.identity': IDENTIFIER_SELECT, + 'openid.ns.pape' : pape.ns_uri, + 'openid.pape.auth_policies': request_args.get('openid.pape.auth_policies', pape.AUTH_NONE), + } + openid_server = self.provider.server + orequest = openid_server.decodeRequest(request) + response = SuccessResponse( + self.endpoint, orequest.message, + signed_fields=['openid.pape.auth_policies',]) + return response + Consumer.complete = mock_complete + + user = User.objects.create_user('testuser', 'test@example.com') + useropenid = UserOpenID( + user=user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + useropenid.save() + + openid_req = {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'} + openid_resp = {'nickname': 'testuser', 'fullname': 'Openid User', + 'email': 'test@example.com'} + + openid_request = self._get_login_request(openid_req) + openid_response = self._get_login_response(openid_request, openid_req, openid_resp, use_pape=pape.AUTH_NONE) + + response_auth = openid_request.message.getArg( + 'http://specs.openid.net/extensions/pape/1.0', + 'auth_policies', + ) + self.assertNotEqual(response_auth, preferred_auth) + + response = self.complete(openid_response) + self.assertEquals(403, response.status_code) + self.assertContains(response, '

OpenID failed

', status_code=403) + self.assertContains(response, '

Login requires physical multi-factor authentication.

', status_code=403) + + def test_login_physical_multifactor_not_provided_override(self): + settings.OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = True + preferred_auth = pape.AUTH_MULTI_FACTOR_PHYSICAL + self.provider.type_uris.append(pape.ns_uri) + + # Override the login_failure handler + def mock_login_failure_handler(request, message, status=403, + template_name=None, + exception=None): + self.assertTrue(isinstance(exception, MissingPhysicalMultiFactor)) + return HttpResponse('Test Failure Override', status=200) + settings.OPENID_RENDER_FAILURE = mock_login_failure_handler + + def mock_complete(this, request_args, return_to): + request = {'openid.mode': 'checkid_setup', + 'openid.trust_root': 'http://localhost/', + 'openid.return_to': 'http://localhost/', + 'openid.identity': IDENTIFIER_SELECT, + 'openid.ns.pape' : pape.ns_uri, + 'openid.pape.auth_policies': request_args.get('openid.pape.auth_policies', pape.AUTH_NONE), + } + openid_server = self.provider.server + orequest = openid_server.decodeRequest(request) + response = SuccessResponse( + self.endpoint, orequest.message, + signed_fields=['openid.pape.auth_policies',]) + return response + Consumer.complete = mock_complete + + user = User.objects.create_user('testuser', 'test@example.com') + useropenid = UserOpenID( + user=user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + useropenid.save() + + openid_req = {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'} + openid_resp = {'nickname': 'testuser', 'fullname': 'Openid User', + 'email': 'test@example.com'} + + openid_request = self._get_login_request(openid_req) + openid_response = self._get_login_response(openid_request, openid_req, openid_resp, use_pape=pape.AUTH_NONE) + + response_auth = openid_request.message.getArg( + 'http://specs.openid.net/extensions/pape/1.0', + 'auth_policies', + ) + self.assertNotEqual(response_auth, preferred_auth) + + # Status code should be 200, since we over-rode the login_failure handler + response = self.complete(openid_response) + self.assertEquals(200, response.status_code) + self.assertContains(response, 'Test Failure Override') + + def test_login_without_nickname(self): + settings.OPENID_CREATE_USERS = True + + openid_req = {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'} + openid_resp = {'nickname': '', 'fullname': 'Openid User', + 'email': 'foo@example.com'} + self._do_user_login(openid_req, openid_resp) + response = self.client.get('/getuser/') + + # username defaults to 'openiduser' + self.assertEquals(response.content, 'openiduser') + + # The user's full name and email have been updated. + user = User.objects.get(username=response.content) + self.assertEquals(user.first_name, 'Openid') + self.assertEquals(user.last_name, 'User') + self.assertEquals(user.email, 'foo@example.com') + + def test_login_without_nickname_with_email_suggestion(self): + settings.OPENID_CREATE_USERS = True + settings.OPENID_USE_EMAIL_FOR_USERNAME = True + + openid_req = {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'} + openid_resp = {'nickname': '', 'fullname': 'Openid User', + 'email': 'foo@example.com'} + self._do_user_login(openid_req, openid_resp) + response = self.client.get('/getuser/') + + # username defaults to a munged version of the email + self.assertEquals(response.content, 'fooexamplecom') + + def test_login_duplicate_username_numbering(self): + settings.OPENID_FOLLOW_RENAMES = False + settings.OPENID_CREATE_USERS = True + settings.OPENID_UPDATE_DETAILS_FROM_SREG = True + # Setup existing user who's name we're going to conflict with + user = User.objects.create_user('testuser', 'someone@example.com') + + # identity url is for 'renameuser' + openid_req = {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'} + # but returned username is for 'testuser', which already exists for another identity + openid_resp = {'nickname': 'testuser', 'fullname': 'Test User', + 'email': 'test@example.com'} + self._do_user_login(openid_req, openid_resp) + response = self.client.get('/getuser/') + + # Since this username is already taken by someone else, we go through + # the process of adding +i to it, and get testuser2. + self.assertEquals(response.content, 'testuser2') + + def test_login_duplicate_username_numbering_with_conflicts(self): + settings.OPENID_FOLLOW_RENAMES = False + settings.OPENID_CREATE_USERS = True + settings.OPENID_UPDATE_DETAILS_FROM_SREG = True + # Setup existing user who's name we're going to conflict with + user = User.objects.create_user('testuser', 'someone@example.com') + user = User.objects.create_user('testuser3', 'someone@example.com') + + # identity url is for 'renameuser' + openid_req = {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'} + # but returned username is for 'testuser', which already exists for another identity + openid_resp = {'nickname': 'testuser', 'fullname': 'Test User', + 'email': 'test@example.com'} + self._do_user_login(openid_req, openid_resp) + response = self.client.get('/getuser/') + + # Since this username is already taken by someone else, we go through + # the process of adding +i to it starting with the count of users with + # username starting with 'testuser', of which there are 2. i should + # start at 3, which already exists, so it should skip to 4. + self.assertEquals(response.content, 'testuser4') + + def test_login_duplicate_username_numbering_with_holes(self): + settings.OPENID_FOLLOW_RENAMES = False + settings.OPENID_CREATE_USERS = True + settings.OPENID_UPDATE_DETAILS_FROM_SREG = True + # Setup existing user who's name we're going to conflict with + user = User.objects.create_user('testuser', 'someone@example.com') + user = User.objects.create_user('testuser1', 'someone@example.com') + user = User.objects.create_user('testuser6', 'someone@example.com') + user = User.objects.create_user('testuser7', 'someone@example.com') + user = User.objects.create_user('testuser8', 'someone@example.com') + + # identity url is for 'renameuser' + openid_req = {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'} + # but returned username is for 'testuser', which already exists for another identity + openid_resp = {'nickname': 'testuser', 'fullname': 'Test User', + 'email': 'test@example.com'} + self._do_user_login(openid_req, openid_resp) + response = self.client.get('/getuser/') + + # Since this username is already taken by someone else, we go through + # the process of adding +i to it starting with the count of users with + # username starting with 'testuser', of which there are 5. i should + # start at 6, and increment until it reaches 9. + self.assertEquals(response.content, 'testuser9') + + def test_login_duplicate_username_numbering_with_nonsequential_matches(self): + settings.OPENID_FOLLOW_RENAMES = False + settings.OPENID_CREATE_USERS = True + settings.OPENID_UPDATE_DETAILS_FROM_SREG = True + # Setup existing user who's name we're going to conflict with + user = User.objects.create_user('testuser', 'someone@example.com') + user = User.objects.create_user('testuserfoo', 'someone@example.com') + + # identity url is for 'renameuser' + openid_req = {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'} + # but returned username is for 'testuser', which already exists for another identity + openid_resp = {'nickname': 'testuser', 'fullname': 'Test User', + 'email': 'test@example.com'} + self._do_user_login(openid_req, openid_resp) + response = self.client.get('/getuser/') + + # Since this username is already taken by someone else, we go through + # the process of adding +i to it starting with the count of users with + # username starting with 'testuser', of which there are 2. i should + # start at 3, which will be available. + self.assertEquals(response.content, 'testuser3') + + def test_login_follow_rename(self): + settings.OPENID_FOLLOW_RENAMES = True + settings.OPENID_UPDATE_DETAILS_FROM_SREG = True + user = User.objects.create_user('testuser', 'someone@example.com') + useropenid = UserOpenID( + user=user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + useropenid.save() + + openid_req = {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'} + openid_resp = {'nickname': 'someuser', 'fullname': 'Some User', + 'email': 'foo@example.com'} + self._do_user_login(openid_req, openid_resp) + response = self.client.get('/getuser/') + + # If OPENID_FOLLOW_RENAMES, they are logged in as + # someuser (the passed in nickname has changed the username) + self.assertEquals(response.content, 'someuser') + + # The user's full name and email have been updated. + user = User.objects.get(username=response.content) + self.assertEquals(user.first_name, 'Some') + self.assertEquals(user.last_name, 'User') + self.assertEquals(user.email, 'foo@example.com') + + def test_login_follow_rename_without_nickname_change(self): + settings.OPENID_FOLLOW_RENAMES = True + settings.OPENID_UPDATE_DETAILS_FROM_SREG = True + settings.OPENID_STRICT_USERNAMES = True + user = User.objects.create_user('testuser', 'someone@example.com') + useropenid = UserOpenID( + user=user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + useropenid.save() + + openid_req = {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'} + openid_resp = {'nickname': 'testuser', 'fullname': 'Some User', + 'email': 'foo@example.com'} + self._do_user_login(openid_req, openid_resp) + response = self.client.get('/getuser/') + + # Username should not have changed + self.assertEquals(response.content, 'testuser') + + # The user's full name and email have been updated. + user = User.objects.get(username=response.content) + self.assertEquals(user.first_name, 'Some') + self.assertEquals(user.last_name, 'User') + self.assertEquals(user.email, 'foo@example.com') + + def test_login_follow_rename_conflict(self): + settings.OPENID_FOLLOW_RENAMES = True + settings.OPENID_UPDATE_DETAILS_FROM_SREG = True + # Setup existing user who's name we're going to switch to + user = User.objects.create_user('testuser', 'someone@example.com') + UserOpenID.objects.get_or_create( + user=user, + claimed_id='http://example.com/existing_identity', + display_id='http://example.com/existing_identity') + + # Setup user who is going to try to change username to 'testuser' + renamed_user = User.objects.create_user('renameuser', 'someone@example.com') + UserOpenID.objects.get_or_create( + user=renamed_user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + + # identity url is for 'renameuser' + openid_req = {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'} + # but returned username is for 'testuser', which already exists for another identity + openid_resp = {'nickname': 'testuser', 'fullname': 'Rename User', + 'email': 'rename@example.com'} + self._do_user_login(openid_req, openid_resp) + response = self.client.get('/getuser/') + + # If OPENID_FOLLOW_RENAMES, attempt to change username to 'testuser' + # but since that username is already taken by someone else, we go through + # the process of adding +i to it, and get testuser2. + self.assertEquals(response.content, 'testuser2') + + # The user's full name and email have been updated. + user = User.objects.get(username=response.content) + self.assertEquals(user.first_name, 'Rename') + self.assertEquals(user.last_name, 'User') + self.assertEquals(user.email, 'rename@example.com') + + def test_login_follow_rename_false_onlyonce(self): + settings.OPENID_FOLLOW_RENAMES = True + settings.OPENID_UPDATE_DETAILS_FROM_SREG = True + # Setup existing user who's name we're going to switch to + user = User.objects.create_user('testuser', 'someone@example.com') + UserOpenID.objects.get_or_create( + user=user, + claimed_id='http://example.com/existing_identity', + display_id='http://example.com/existing_identity') + + # Setup user who is going to try to change username to 'testuser' + renamed_user = User.objects.create_user('testuser2000eight', 'someone@example.com') + UserOpenID.objects.get_or_create( + user=renamed_user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + + # identity url is for 'testuser2000eight' + openid_req = {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'} + # but returned username is for 'testuser', which already exists for another identity + openid_resp = {'nickname': 'testuser2', 'fullname': 'Rename User', + 'email': 'rename@example.com'} + self._do_user_login(openid_req, openid_resp) + response = self.client.get('/getuser/') + + # If OPENID_FOLLOW_RENAMES, attempt to change username to 'testuser' + # but since that username is already taken by someone else, we go through + # the process of adding +i to it. Even though it looks like the username + # follows the nickname+i scheme, it has non-numbers in the suffix, so + # it's not an auto-generated one. The regular process of renaming to + # 'testuser' has a conflict, so we get +2 at the end. + self.assertEquals(response.content, 'testuser2') + + # The user's full name and email have been updated. + user = User.objects.get(username=response.content) + self.assertEquals(user.first_name, 'Rename') + self.assertEquals(user.last_name, 'User') + self.assertEquals(user.email, 'rename@example.com') + + def test_login_follow_rename_conflict_onlyonce(self): + settings.OPENID_FOLLOW_RENAMES = True + settings.OPENID_UPDATE_DETAILS_FROM_SREG = True + # Setup existing user who's name we're going to switch to + user = User.objects.create_user('testuser', 'someone@example.com') + UserOpenID.objects.get_or_create( + user=user, + claimed_id='http://example.com/existing_identity', + display_id='http://example.com/existing_identity') + + # Setup user who is going to try to change username to 'testuser' + renamed_user = User.objects.create_user('testuser2000', 'someone@example.com') + UserOpenID.objects.get_or_create( + user=renamed_user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + + # identity url is for 'testuser2000' + openid_req = {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'} + # but returned username is for 'testuser', which already exists for another identity + openid_resp = {'nickname': 'testuser', 'fullname': 'Rename User', + 'email': 'rename@example.com'} + self._do_user_login(openid_req, openid_resp) + response = self.client.get('/getuser/') + + # If OPENID_FOLLOW_RENAMES, attempt to change username to 'testuser' + # but since that username is already taken by someone else, we go through + # the process of adding +i to it. Since the user for this identity url + # already has a name matching that pattern, check if first. + self.assertEquals(response.content, 'testuser2000') + + # The user's full name and email have been updated. + user = User.objects.get(username=response.content) + self.assertEquals(user.first_name, 'Rename') + self.assertEquals(user.last_name, 'User') + self.assertEquals(user.email, 'rename@example.com') + + def test_login_follow_rename_false_conflict(self): + settings.OPENID_FOLLOW_RENAMES = True + settings.OPENID_UPDATE_DETAILS_FROM_SREG = True + # Setup existing user who's username matches the name+i pattern + user = User.objects.create_user('testuser2', 'someone@example.com') + UserOpenID.objects.get_or_create( + user=user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + + # identity url is for 'testuser2' + openid_req = {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'} + # but returned username is for 'testuser', which looks like we've done + # a username+1 for them already, but 'testuser' isn't actually taken + openid_resp = {'nickname': 'testuser', 'fullname': 'Same User', + 'email': 'same@example.com'} + self._do_user_login(openid_req, openid_resp) + response = self.client.get('/getuser/') + + # If OPENID_FOLLOW_RENAMES, username should be changed to 'testuser' + # because it wasn't currently taken + self.assertEquals(response.content, 'testuser') + + # The user's full name and email have been updated. + user = User.objects.get(username=response.content) + self.assertEquals(user.first_name, 'Same') + self.assertEquals(user.last_name, 'User') + self.assertEquals(user.email, 'same@example.com') + + def test_strict_username_no_nickname(self): + settings.OPENID_CREATE_USERS = True + settings.OPENID_STRICT_USERNAMES = True + settings.OPENID_SREG_REQUIRED_FIELDS = [] + + # Posting in an identity URL begins the authentication request: + response = self.client.post('/openid/login/', + {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'}) + self.assertContains(response, 'OpenID transaction in progress') + + # Complete the request, passing back some simple registration + # data. The user is redirected to the next URL. + openid_request = self.provider.parseFormPost(response.content) + sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request) + openid_response = openid_request.answer(True) + sreg_response = sreg.SRegResponse.extractResponse( + sreg_request, {'nickname': '', # No nickname + 'fullname': 'Some User', + 'email': 'foo@example.com'}) + openid_response.addExtension(sreg_response) + response = self.complete(openid_response) + + # Status code should be 403: Forbidden + self.assertEquals(403, response.status_code) + self.assertContains(response, '

OpenID failed

', status_code=403) + self.assertContains(response, "An attribute required for logging in was not returned " + "(nickname)", status_code=403) + + def test_strict_username_no_nickname_override(self): + settings.OPENID_CREATE_USERS = True + settings.OPENID_STRICT_USERNAMES = True + settings.OPENID_SREG_REQUIRED_FIELDS = [] + + # Override the login_failure handler + def mock_login_failure_handler(request, message, status=403, + template_name=None, + exception=None): + self.assertTrue(isinstance(exception, (RequiredAttributeNotReturned, MissingUsernameViolation))) + return HttpResponse('Test Failure Override', status=200) + settings.OPENID_RENDER_FAILURE = mock_login_failure_handler + + # Posting in an identity URL begins the authentication request: + response = self.client.post('/openid/login/', + {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'}) + self.assertContains(response, 'OpenID transaction in progress') + + # Complete the request, passing back some simple registration + # data. The user is redirected to the next URL. + openid_request = self.provider.parseFormPost(response.content) + sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request) + openid_response = openid_request.answer(True) + sreg_response = sreg.SRegResponse.extractResponse( + sreg_request, {'nickname': '', # No nickname + 'fullname': 'Some User', + 'email': 'foo@example.com'}) + openid_response.addExtension(sreg_response) + response = self.complete(openid_response) + + # Status code should be 200, since we over-rode the login_failure handler + self.assertEquals(200, response.status_code) + self.assertContains(response, 'Test Failure Override') + + def test_strict_username_duplicate_user(self): + settings.OPENID_CREATE_USERS = True + settings.OPENID_STRICT_USERNAMES = True + # Create a user with the same name as we'll pass back via sreg. + user = User.objects.create_user('someuser', 'someone@example.com') + useropenid = UserOpenID( + user=user, + claimed_id='http://example.com/different_identity', + display_id='http://example.com/different_identity') + useropenid.save() + + # Posting in an identity URL begins the authentication request: + response = self.client.post('/openid/login/', + {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'}) + self.assertContains(response, 'OpenID transaction in progress') + + # Complete the request, passing back some simple registration + # data. The user is redirected to the next URL. + openid_request = self.provider.parseFormPost(response.content) + sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request) + openid_response = openid_request.answer(True) + sreg_response = sreg.SRegResponse.extractResponse( + sreg_request, {'nickname': 'someuser', 'fullname': 'Some User', + 'email': 'foo@example.com'}) + openid_response.addExtension(sreg_response) + response = self.complete(openid_response) + + # Status code should be 403: Forbidden + self.assertEquals(403, response.status_code) + self.assertContains(response, '

OpenID failed

', status_code=403) + self.assertContains(response, + "The username (someuser) with which you tried to log in is " + "already in use for a different account.", + status_code=403) + + def test_strict_username_duplicate_user_override(self): + settings.OPENID_CREATE_USERS = True + settings.OPENID_STRICT_USERNAMES = True + + # Override the login_failure handler + def mock_login_failure_handler(request, message, status=403, + template_name=None, + exception=None): + self.assertTrue(isinstance(exception, DuplicateUsernameViolation)) + return HttpResponse('Test Failure Override', status=200) + settings.OPENID_RENDER_FAILURE = mock_login_failure_handler + + # Create a user with the same name as we'll pass back via sreg. + user = User.objects.create_user('someuser', 'someone@example.com') + useropenid = UserOpenID( + user=user, + claimed_id='http://example.com/different_identity', + display_id='http://example.com/different_identity') + useropenid.save() + + # Posting in an identity URL begins the authentication request: + response = self.client.post('/openid/login/', + {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'}) + self.assertContains(response, 'OpenID transaction in progress') + + # Complete the request, passing back some simple registration + # data. The user is redirected to the next URL. + openid_request = self.provider.parseFormPost(response.content) + sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request) + openid_response = openid_request.answer(True) + sreg_response = sreg.SRegResponse.extractResponse( + sreg_request, {'nickname': 'someuser', 'fullname': 'Some User', + 'email': 'foo@example.com'}) + openid_response.addExtension(sreg_response) + response = self.complete(openid_response) + + # Status code should be 200, since we over-rode the login_failure handler + self.assertEquals(200, response.status_code) + self.assertContains(response, 'Test Failure Override') + + def test_login_requires_sreg_required_fields(self): + # If any required attributes are not included in the response, + # we fail with a forbidden. + settings.OPENID_CREATE_USERS = True + settings.OPENID_SREG_REQUIRED_FIELDS = ('email', 'language') + # Posting in an identity URL begins the authentication request: + response = self.client.post('/openid/login/', + {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'}) + self.assertContains(response, 'OpenID transaction in progress') + + # Complete the request, passing back some simple registration + # data. The user is redirected to the next URL. + openid_request = self.provider.parseFormPost(response.content) + sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request) + openid_response = openid_request.answer(True) + sreg_response = sreg.SRegResponse.extractResponse( + sreg_request, {'nickname': 'foo', + 'fullname': 'Some User', + 'email': 'foo@example.com'}) + openid_response.addExtension(sreg_response) + response = self.complete(openid_response) + + # Status code should be 403: Forbidden as we didn't include + # a required field - language. + self.assertContains(response, + "An attribute required for logging in was not returned " + "(language)", status_code=403) + + def test_login_update_details(self): + settings.OPENID_UPDATE_DETAILS_FROM_SREG = True + user = User.objects.create_user('testuser', 'someone@example.com') + useropenid = UserOpenID( + user=user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + useropenid.save() + + openid_req = {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'} + openid_resp = {'nickname': 'testuser', 'fullname': 'Some User', + 'email': 'foo@example.com'} + self._do_user_login(openid_req, openid_resp) + response = self.client.get('/getuser/') + + self.assertEquals(response.content, 'testuser') + + # The user's full name and email have been updated. + user = User.objects.get(username=response.content) + self.assertEquals(user.first_name, 'Some') + self.assertEquals(user.last_name, 'User') + self.assertEquals(user.email, 'foo@example.com') + + def test_login_uses_sreg_extra_fields(self): + # The configurable sreg attributes are used in the request. + settings.OPENID_SREG_EXTRA_FIELDS = ('language',) + user = User.objects.create_user('testuser', 'someone@example.com') + useropenid = UserOpenID( + user=user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + useropenid.save() + + # Posting in an identity URL begins the authentication request: + response = self.client.post('/openid/login/', + {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'}) + + openid_request = self.provider.parseFormPost(response.content) + sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request) + for field in ('email', 'fullname', 'nickname', 'language'): + self.assertTrue(field in sreg_request) + + def test_login_uses_sreg_required_fields(self): + # The configurable sreg attributes are used in the request. + settings.OPENID_SREG_REQUIRED_FIELDS = ('email', 'language') + user = User.objects.create_user('testuser', 'someone@example.com') + useropenid = UserOpenID( + user=user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + useropenid.save() + + # Posting in an identity URL begins the authentication request: + response = self.client.post('/openid/login/', + {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'}) + + openid_request = self.provider.parseFormPost(response.content) + sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request) + + self.assertEqual(['email', 'language'], sreg_request.required) + self.assertEqual(['fullname', 'nickname'], sreg_request.optional) + + def test_login_attribute_exchange(self): + settings.OPENID_UPDATE_DETAILS_FROM_SREG = True + user = User.objects.create_user('testuser', 'someone@example.com') + useropenid = UserOpenID( + user=user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + useropenid.save() + + # Configure the provider to advertise attribute exchange + # protocol and start the authentication process: + self.provider.type_uris.append('http://openid.net/srv/ax/1.0') + response = self.client.post('/openid/login/', + {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'}) + self.assertContains(response, 'OpenID transaction in progress') + + # The resulting OpenID request uses the Attribute Exchange + # extension rather than the Simple Registration extension. + openid_request = self.provider.parseFormPost(response.content) + sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request) + self.assertEqual(sreg_request.required, []) + self.assertEqual(sreg_request.optional, []) + + fetch_request = ax.FetchRequest.fromOpenIDRequest(openid_request) + self.assertTrue(fetch_request.has_key( + 'http://axschema.org/contact/email')) + self.assertTrue(fetch_request.has_key( + 'http://axschema.org/namePerson')) + self.assertTrue(fetch_request.has_key( + 'http://axschema.org/namePerson/first')) + self.assertTrue(fetch_request.has_key( + 'http://axschema.org/namePerson/last')) + self.assertTrue(fetch_request.has_key( + 'http://axschema.org/namePerson/friendly')) + # myOpenID compatibilty attributes: + self.assertTrue(fetch_request.has_key( + 'http://schema.openid.net/contact/email')) + self.assertTrue(fetch_request.has_key( + 'http://schema.openid.net/namePerson')) + self.assertTrue(fetch_request.has_key( + 'http://schema.openid.net/namePerson/friendly')) + + # Build up a response including AX data. + openid_response = openid_request.answer(True) + fetch_response = ax.FetchResponse(fetch_request) + fetch_response.addValue( + 'http://axschema.org/contact/email', 'foo@example.com') + fetch_response.addValue( + 'http://axschema.org/namePerson/first', 'Firstname') + fetch_response.addValue( + 'http://axschema.org/namePerson/last', 'Lastname') + fetch_response.addValue( + 'http://axschema.org/namePerson/friendly', 'someuser') + openid_response.addExtension(fetch_response) + response = self.complete(openid_response) + self.assertRedirects(response, 'http://testserver/getuser/') + + # And they are now logged in as testuser (the passed in + # nickname has not caused the username to change). + response = self.client.get('/getuser/') + self.assertEquals(response.content, 'testuser') + + # The user's full name and email have been updated. + user = User.objects.get(username='testuser') + self.assertEquals(user.first_name, 'Firstname') + self.assertEquals(user.last_name, 'Lastname') + self.assertEquals(user.email, 'foo@example.com') + + def test_login_teams(self): + settings.OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO = False + settings.OPENID_LAUNCHPAD_TEAMS_MAPPING = {'teamname': 'groupname', + 'otherteam': 'othergroup'} + user = User.objects.create_user('testuser', 'someone@example.com') + group = Group(name='groupname') + group.save() + ogroup = Group(name='othergroup') + ogroup.save() + user.groups.add(ogroup) + user.save() + useropenid = UserOpenID( + user=user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + useropenid.save() + + # Posting in an identity URL begins the authentication request: + response = self.client.post('/openid/login/', + {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'}) + self.assertContains(response, 'OpenID transaction in progress') + + # Complete the request + openid_request = self.provider.parseFormPost(response.content) + openid_response = openid_request.answer(True) + teams_request = teams.TeamsRequest.fromOpenIDRequest(openid_request) + teams_response = teams.TeamsResponse.extractResponse( + teams_request, 'teamname,some-other-team') + openid_response.addExtension(teams_response) + response = self.complete(openid_response) + self.assertRedirects(response, 'http://testserver/getuser/') + + # And they are now logged in as testuser + response = self.client.get('/getuser/') + self.assertEquals(response.content, 'testuser') + + # The user's groups have been updated. + user = User.objects.get(username='testuser') + self.assertTrue(group in user.groups.all()) + self.assertTrue(ogroup not in user.groups.all()) + + def test_login_teams_automapping(self): + settings.OPENID_LAUNCHPAD_TEAMS_MAPPING = {'teamname': 'groupname', + 'otherteam': 'othergroup'} + settings.OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO = True + settings.OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST = ['django-group1', 'django-group2'] + user = User.objects.create_user('testuser', 'someone@example.com') + group1 = Group(name='django-group1') + group1.save() + group2 = Group(name='django-group2') + group2.save() + group3 = Group(name='django-group3') + group3.save() + user.save() + useropenid = UserOpenID( + user=user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + useropenid.save() + + # Posting in an identity URL begins the authentication request: + response = self.client.post('/openid/login/', + {'openid_identifier': 'http://example.com/identity', + 'next': '/getuser/'}) + self.assertContains(response, 'OpenID transaction in progress') + + # Complete the request + openid_request = self.provider.parseFormPost(response.content) + openid_response = openid_request.answer(True) + teams_request = teams.TeamsRequest.fromOpenIDRequest(openid_request) + + self.assertEqual(group1 in user.groups.all(), False) + self.assertEqual(group2 in user.groups.all(), False) + self.assertTrue(group3 not in user.groups.all()) + + def test_login_teams_staff_not_defined(self): + delattr(settings, 'OPENID_LAUNCHPAD_STAFF_TEAMS') + user = User.objects.create_user('testuser', 'someone@example.com') + user.is_staff = True + user.save() + self.assertTrue(user.is_staff) + + user = self.get_openid_authed_user_with_teams(user, 'teamname,some-other-team') + self.assertTrue(user.is_staff) + + def test_login_teams_staff_assignment(self): + settings.OPENID_LAUNCHPAD_STAFF_TEAMS = ('teamname',) + user = User.objects.create_user('testuser', 'someone@example.com') + user.is_staff = False + user.save() + self.assertFalse(user.is_staff) + + user = self.get_openid_authed_user_with_teams(user, 'teamname,some-other-team') + self.assertTrue(user.is_staff) + + def test_login_teams_staff_unassignment(self): + settings.OPENID_LAUNCHPAD_STAFF_TEAMS = ('different-teamname',) + user = User.objects.create_user('testuser', 'someone@example.com') + user.is_staff = True + user.save() + self.assertTrue(user.is_staff) + + user = self.get_openid_authed_user_with_teams(user, 'teamname,some-other-team') + self.assertFalse(user.is_staff) + + def get_openid_authed_user_with_teams(self, user, teams_str): + useropenid = UserOpenID( + user=user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + useropenid.save() + + # Posting in an identity URL begins the authentication request: + response = self.client.post('/openid/login/', + {'openid_identifier': 'http://example.com/identity'}) + + # Complete the request + openid_request = self.provider.parseFormPost(response.content) + openid_response = openid_request.answer(True) + teams_request = teams.TeamsRequest.fromOpenIDRequest(openid_request) + teams_response = teams.TeamsResponse.extractResponse( + teams_request, teams_str) + openid_response.addExtension(teams_response) + response = self.complete(openid_response) + return User.objects.get(username=user.username) + + def test_login_complete_signals_login(self): + # An oauth_login_complete signal is emitted including the + # request and sreg_response. + user = User.objects.create_user('someuser', 'someone@example.com') + useropenid = UserOpenID( + user=user, + claimed_id='http://example.com/identity', + display_id='http://example.com/identity') + useropenid.save() + response = self.client.post('/openid/login/', + {'openid_identifier': 'http://example.com/identity'}) + openid_request = self.provider.parseFormPost(response.content) + openid_response = openid_request.answer(True) + # Use a closure to test whether the signal handler was called. + self.signal_handler_called = False + def login_callback(sender, **kwargs): + self.assertTrue(isinstance( + kwargs.get('request', None), HttpRequest)) + self.assertTrue(isinstance( + kwargs.get('openid_response', None), SuccessResponse)) + self.signal_handler_called = True + openid_login_complete.connect(login_callback) + + response = self.complete(openid_response) + + self.assertTrue(self.signal_handler_called) + openid_login_complete.disconnect(login_callback) + + +class HelperFunctionsTest(TestCase): + def test_sanitise_redirect_url(self): + settings.ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = [ + "example.com", "example.org"] + # list of URLs and whether they should be passed or not + urls = [ + ("http://example.com", True), + ("http://example.org/", True), + ("http://example.org/foo/bar", True), + ("http://example.org/foo/bar?baz=quux", True), + ("http://example.org:9999/foo/bar?baz=quux", True), + ("http://www.example.org/", False), + ("http://example.net/foo/bar?baz=quux", False), + ("/somewhere/local", True), + ("/somewhere/local?url=http://fail.com/bar", True), + # An empty path, as seen when no "next" parameter is passed. + ("", False), + ("/path with spaces", False), + ] + for url, returns_self in urls: + sanitised = sanitise_redirect_url(url) + if returns_self: + self.assertEqual(url, sanitised) + else: + self.assertEqual(settings.LOGIN_REDIRECT_URL, sanitised) + +def suite(): + return unittest.TestLoader().loadTestsFromName(__name__) diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/urls.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/urls.py new file mode 100644 index 0000000..829958c --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/tests/urls.py @@ -0,0 +1,39 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2009-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from django.http import HttpResponse +from django.conf.urls.defaults import * + + +def get_user(request): + return HttpResponse(request.user.username) + +urlpatterns = patterns('', + (r'^getuser/$', get_user), + (r'^openid/', include('django_openid_auth.urls')), +) diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/urls.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/urls.py new file mode 100644 index 0000000..d7f4e32 --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/urls.py @@ -0,0 +1,36 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2007 Simon Willison +# Copyright (C) 2008-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from django.conf.urls.defaults import * + +urlpatterns = patterns('django_openid_auth.views', + url(r'^login/$', 'login_begin', name='openid-login'), + url(r'^complete/$', 'login_complete', name='openid-complete'), + url(r'^logo.gif$', 'logo', name='openid-logo'), +) diff --git a/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/views.py b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/views.py new file mode 100644 index 0000000..8585960 --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/django_openid_auth/views.py @@ -0,0 +1,313 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2007 Simon Willison +# Copyright (C) 2008-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import re +import urllib +from urlparse import urlsplit + +from django.conf import settings +from django.contrib.auth import ( + REDIRECT_FIELD_NAME, authenticate, login as auth_login) +from django.contrib.auth.models import Group +from django.core.urlresolvers import reverse +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.template.loader import render_to_string +try: + from django.views.decorators.csrf import csrf_exempt +except ImportError: + from django.contrib.csrf.middleware import csrf_exempt + +from openid.consumer.consumer import ( + Consumer, SUCCESS, CANCEL, FAILURE) +from openid.consumer.discover import DiscoveryFailure +from openid.extensions import sreg, ax, pape + +from django_openid_auth import teams +from django_openid_auth.forms import OpenIDLoginForm +from django_openid_auth.models import UserOpenID +from django_openid_auth.signals import openid_login_complete +from django_openid_auth.store import DjangoOpenIDStore +from django_openid_auth.exceptions import ( + RequiredAttributeNotReturned, + DjangoOpenIDException, +) + + +next_url_re = re.compile('^/[-\w/]+$') + +def is_valid_next_url(next): + # When we allow this: + # /openid/?next=/welcome/ + # For security reasons we want to restrict the next= bit to being a local + # path, not a complete URL. + return bool(next_url_re.match(next)) + + +def sanitise_redirect_url(redirect_to): + """Sanitise the redirection URL.""" + # Light security check -- make sure redirect_to isn't garbage. + is_valid = True + if not redirect_to or ' ' in redirect_to: + is_valid = False + elif '//' in redirect_to: + # Allow the redirect URL to be external if it's a permitted domain + allowed_domains = getattr(settings, + "ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS", []) + s, netloc, p, q, f = urlsplit(redirect_to) + # allow it if netloc is blank or if the domain is allowed + if netloc: + # a domain was specified. Is it an allowed domain? + if netloc.find(":") != -1: + netloc, _ = netloc.split(":", 1) + if netloc not in allowed_domains: + is_valid = False + + # If the return_to URL is not valid, use the default. + if not is_valid: + redirect_to = settings.LOGIN_REDIRECT_URL + + return redirect_to + + +def make_consumer(request): + """Create an OpenID Consumer object for the given Django request.""" + # Give the OpenID library its own space in the session object. + session = request.session.setdefault('OPENID', {}) + store = DjangoOpenIDStore() + return Consumer(session, store) + + +def render_openid_request(request, openid_request, return_to, trust_root=None): + """Render an OpenID authentication request.""" + if trust_root is None: + trust_root = getattr(settings, 'OPENID_TRUST_ROOT', + request.build_absolute_uri('/')) + + if openid_request.shouldSendRedirect(): + redirect_url = openid_request.redirectURL( + trust_root, return_to) + return HttpResponseRedirect(redirect_url) + else: + form_html = openid_request.htmlMarkup( + trust_root, return_to, form_tag_attrs={'id': 'openid_message'}) + return HttpResponse(form_html, content_type='text/html;charset=UTF-8') + + +def default_render_failure(request, message, status=403, + template_name='openid/failure.html', + exception=None): + """Render an error page to the user.""" + data = render_to_string( + template_name, dict(message=message, exception=exception), + context_instance=RequestContext(request)) + return HttpResponse(data, status=status) + + +def parse_openid_response(request): + """Parse an OpenID response from a Django request.""" + # Short cut if there is no request parameters. + #if len(request.REQUEST) == 0: + # return None + + current_url = request.build_absolute_uri() + + consumer = make_consumer(request) + return consumer.complete(dict(request.REQUEST.items()), current_url) + + +def login_begin(request, template_name='openid/login.html', + login_complete_view='openid-complete', + form_class=OpenIDLoginForm, + render_failure=default_render_failure, + redirect_field_name=REDIRECT_FIELD_NAME): + """Begin an OpenID login request, possibly asking for an identity URL.""" + redirect_to = request.REQUEST.get(redirect_field_name, '') + + # Get the OpenID URL to try. First see if we've been configured + # to use a fixed server URL. + openid_url = getattr(settings, 'OPENID_SSO_SERVER_URL', None) + + if openid_url is None: + if request.POST: + login_form = form_class(data=request.POST) + if login_form.is_valid(): + openid_url = login_form.cleaned_data['openid_identifier'] + else: + login_form = form_class() + + # Invalid or no form data: + if openid_url is None: + return render_to_response(template_name, { + 'form': login_form, + redirect_field_name: redirect_to + }, context_instance=RequestContext(request)) + + error = None + consumer = make_consumer(request) + try: + openid_request = consumer.begin(openid_url) + except DiscoveryFailure, exc: + return render_failure( + request, "OpenID discovery error: %s" % (str(exc),), status=500, + exception=exc) + + # Request some user details. If the provider advertises support + # for attribute exchange, use that. + if openid_request.endpoint.supportsType(ax.AXMessage.ns_uri): + fetch_request = ax.FetchRequest() + # We mark all the attributes as required, since Google ignores + # optional attributes. We request both the full name and + # first/last components since some providers offer one but not + # the other. + for (attr, alias) in [ + ('http://axschema.org/contact/email', 'email'), + ('http://axschema.org/namePerson', 'fullname'), + ('http://axschema.org/namePerson/first', 'firstname'), + ('http://axschema.org/namePerson/last', 'lastname'), + ('http://axschema.org/namePerson/friendly', 'nickname'), + # The myOpenID provider advertises AX support, but uses + # attribute names from an obsolete draft of the + # specification. We request them for compatibility. + ('http://schema.openid.net/contact/email', 'old_email'), + ('http://schema.openid.net/namePerson', 'old_fullname'), + ('http://schema.openid.net/namePerson/friendly', 'old_nickname')]: + fetch_request.add(ax.AttrInfo(attr, alias=alias, required=True)) + openid_request.addExtension(fetch_request) + else: + sreg_required_fields = [] + sreg_required_fields.extend( + getattr(settings, 'OPENID_SREG_REQUIRED_FIELDS', [])) + sreg_optional_fields = ['email', 'fullname', 'nickname'] + sreg_optional_fields.extend( + getattr(settings, 'OPENID_SREG_EXTRA_FIELDS', [])) + sreg_optional_fields = [ + field for field in sreg_optional_fields if ( + not field in sreg_required_fields)] + openid_request.addExtension( + sreg.SRegRequest(optional=sreg_optional_fields, + required=sreg_required_fields)) + + if getattr(settings, 'OPENID_PHYSICAL_MULTIFACTOR_REQUIRED', False): + preferred_auth = [ + pape.AUTH_MULTI_FACTOR_PHYSICAL, + ] + pape_request = pape.Request(preferred_auth_policies=preferred_auth) + openid_request.addExtension(pape_request) + + # Request team info + teams_mapping_auto = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO', False) + teams_mapping_auto_blacklist = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST', []) + launchpad_teams = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {}) + if teams_mapping_auto: + #ignore launchpad teams. use all django-groups + launchpad_teams = dict() + all_groups = Group.objects.exclude(name__in=teams_mapping_auto_blacklist) + for group in all_groups: + launchpad_teams[group.name] = group.name + + if launchpad_teams: + openid_request.addExtension(teams.TeamsRequest(launchpad_teams.keys())) + + # Construct the request completion URL, including the page we + # should redirect to. + return_to = request.build_absolute_uri(reverse(login_complete_view)) + if redirect_to: + if '?' in return_to: + return_to += '&' + else: + return_to += '?' + # Django gives us Unicode, which is great. We must encode URI. + # urllib enforces str. We can't trust anything about the default + # encoding inside str(foo) , so we must explicitly make foo a str. + return_to += urllib.urlencode( + {redirect_field_name: redirect_to.encode("UTF-8")}) + + return render_openid_request(request, openid_request, return_to) + + +@csrf_exempt +def login_complete(request, redirect_field_name=REDIRECT_FIELD_NAME, + render_failure=None): + redirect_to = request.REQUEST.get(redirect_field_name, '') + render_failure = render_failure or \ + getattr(settings, 'OPENID_RENDER_FAILURE', None) or \ + default_render_failure + + openid_response = parse_openid_response(request) + if not openid_response: + return render_failure( + request, 'This is an OpenID relying party endpoint.') + + if openid_response.status == SUCCESS: + try: + user = authenticate(openid_response=openid_response) + except DjangoOpenIDException, e: + return render_failure(request, e.message, exception=e) + + if user is not None: + if user.is_active: + auth_login(request, user) + response = HttpResponseRedirect(sanitise_redirect_url(redirect_to)) + + # Notify any listeners that we successfully logged in. + openid_login_complete.send(sender=UserOpenID, request=request, + openid_response=openid_response) + + return response + else: + return render_failure(request, 'Disabled account') + else: + return render_failure(request, 'Unknown user') + elif openid_response.status == FAILURE: + return render_failure( + request, 'OpenID authentication failed: %s' % + openid_response.message) + elif openid_response.status == CANCEL: + return render_failure(request, 'Authentication cancelled') + else: + assert False, ( + "Unknown OpenID response type: %r" % openid_response.status) + + +def logo(request): + return HttpResponse( + OPENID_LOGO_BASE_64.decode('base64'), mimetype='image/gif' + ) + +# Logo from http://openid.net/login-bg.gif +# Embedded here for convenience; you should serve this as a static file +OPENID_LOGO_BASE_64 = """ +R0lGODlhEAAQAMQAAO3t7eHh4srKyvz8/P5pDP9rENLS0v/28P/17tXV1dHEvPDw8M3Nzfn5+d3d +3f5jA97Syvnv6MfLzcfHx/1mCPx4Kc/S1Pf189C+tP+xgv/k1N3OxfHy9NLV1/39/f///yH5BAAA +AAAALAAAAAAQABAAAAVq4CeOZGme6KhlSDoexdO6H0IUR+otwUYRkMDCUwIYJhLFTyGZJACAwQcg +EAQ4kVuEE2AIGAOPQQAQwXCfS8KQGAwMjIYIUSi03B7iJ+AcnmclHg4TAh0QDzIpCw4WGBUZeikD +Fzk0lpcjIQA7 +""" diff --git a/desktop/core/ext-py/django-openid-auth-0.5/example_consumer/__init__.py b/desktop/core/ext-py/django-openid-auth-0.5/example_consumer/__init__.py new file mode 100644 index 0000000..ca9566c --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/example_consumer/__init__.py @@ -0,0 +1,28 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2007 Simon Willison +# Copyright (C) 2008-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. diff --git a/desktop/core/ext-py/django-openid-auth-0.5/example_consumer/manage.py b/desktop/core/ext-py/django-openid-auth-0.5/example_consumer/manage.py new file mode 100755 index 0000000..5e78ea9 --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/example_consumer/manage.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +from django.core.management import execute_manager +try: + import settings # Assumed to be in the same directory. +except ImportError: + import sys + sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) + sys.exit(1) + +if __name__ == "__main__": + execute_manager(settings) diff --git a/desktop/core/ext-py/django-openid-auth-0.5/example_consumer/settings.py b/desktop/core/ext-py/django-openid-auth-0.5/example_consumer/settings.py new file mode 100644 index 0000000..1b5ea66 --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/example_consumer/settings.py @@ -0,0 +1,145 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2007 Simon Willison +# Copyright (C) 2008-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# Django settings for example project. +import django +django_version = django.get_version() +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@domain.com'), +) + +MANAGERS = ADMINS + +if django_version >= "1.2": + csrf_middleware = 'django.middleware.csrf.CsrfViewMiddleware' + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'sqlite.db' + } + } + TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + ) +else: + csrf_middleware = 'django.contrib.csrf.middleware.CsrfViewMiddleware' + TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.load_template_source', + 'django.template.loaders.app_directories.load_template_source', + ) + DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'. + DATABASE_NAME = 'sqlite.db' # Or path to database file if using sqlite3. + DATABASE_USER = '' # Not used with sqlite3. + DATABASE_PASSWORD = '' # Not used with sqlite3. + DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. + DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. + +# Local time zone for this installation. Choices can be found here: +# http://www.postgresql.org/docs/8.1/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE +# although not all variations may be possible on all operating systems. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'America/Chicago' + +# Language code for this installation. All choices can be found here: +# http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes +# http://blogs.law.harvard.edu/tech/stories/storyReader$15 +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. +# Example: "http://media.lawrence.com" +MEDIA_URL = '' + +# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a +# trailing slash. +# Examples: "http://foo.com/media/", "/media/". +ADMIN_MEDIA_PREFIX = '/media/' + +# Make this unique, and don't share it with anybody. +SECRET_KEY = '34958734985734985734985798437' + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + csrf_middleware, +) + +ROOT_URLCONF = 'example_consumer.urls' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.admin', + 'django_openid_auth', +) + +AUTHENTICATION_BACKENDS = ( + 'django_openid_auth.auth.OpenIDBackend', + 'django.contrib.auth.backends.ModelBackend', +) + +# Should users be created when new OpenIDs are used to log in? +OPENID_CREATE_USERS = True + +# When logging in again, should we overwrite user details based on +# data received via Simple Registration? +OPENID_UPDATE_DETAILS_FROM_SREG = True + +# If set, always use this as the identity URL rather than asking the +# user. This only makes sense if it is a server URL. +OPENID_SSO_SERVER_URL = 'https://login.launchpad.net/' + +# Tell django.contrib.auth to use the OpenID signin URLs. +LOGIN_URL = '/openid/login/' +LOGIN_REDIRECT_URL = '/' + +# Should django_auth_openid be used to sign into the admin interface? +OPENID_USE_AS_ADMIN_LOGIN = False diff --git a/desktop/core/ext-py/django-openid-auth-0.5/example_consumer/urls.py b/desktop/core/ext-py/django-openid-auth-0.5/example_consumer/urls.py new file mode 100644 index 0000000..6132fd3 --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/example_consumer/urls.py @@ -0,0 +1,45 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2007 Simon Willison +# Copyright (C) 2008-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from django.conf.urls.defaults import * +from django.contrib import admin + +import views + + +admin.autodiscover() + +urlpatterns = patterns('', + (r'^$', views.index), + (r'^openid/', include('django_openid_auth.urls')), + (r'^logout/$', 'django.contrib.auth.views.logout'), + (r'^private/$', views.require_authentication), + + (r'^admin/', include(admin.site.urls)), +) diff --git a/desktop/core/ext-py/django-openid-auth-0.5/example_consumer/views.py b/desktop/core/ext-py/django-openid-auth-0.5/example_consumer/views.py new file mode 100644 index 0000000..c86dcc6 --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/example_consumer/views.py @@ -0,0 +1,57 @@ +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2007 Simon Willison +# Copyright (C) 2008-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from django.contrib.auth.decorators import login_required +from django.http import HttpResponse +from django.utils.html import escape + + +def index(request): + s = ['

'] + if request.user.is_authenticated(): + s.append('You are signed in as %s (%s)' % ( + escape(request.user.username), + escape(request.user.get_full_name()))) + s.append(' | Sign out') + else: + s.append('Sign in with OpenID') + + s.append('

') + + s.append('

This requires authentication

') + return HttpResponse('\n'.join(s)) + + +def next_works(request): + return HttpResponse('?next= bit works. Home') + + +@login_required +def require_authentication(request): + return HttpResponse('This page requires authentication') diff --git a/desktop/core/ext-py/django-openid-auth-0.5/setup.py b/desktop/core/ext-py/django-openid-auth-0.5/setup.py new file mode 100644 index 0000000..fb2829f --- /dev/null +++ b/desktop/core/ext-py/django-openid-auth-0.5/setup.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# django-openid-auth - OpenID integration for django.contrib.auth +# +# Copyright (C) 2009-2013 Canonical Ltd. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +"""OpenID integration for django.contrib.auth + +A library that can be used to add OpenID support to Django applications. +The library integrates with Django's built in authentication system, so +most applications require minimal changes to support OpenID llogin. The +library also includes the following features: + * Basic user details are transferred from the OpenID server via the + Simple Registration extension or Attribute Exchange extension. + * can be configured to use a fixed OpenID server URL, for use in SSO. + * supports the launchpad.net teams extension to get team membership + info. +""" + +from distutils.core import setup + + +description, long_description = __doc__.split('\n\n', 1) +VERSION = '0.5' + +setup( + name='django-openid-auth', + version=VERSION, + author='Canonical Ltd', + description=description, + long_description=long_description, + license='BSD', + platforms=['any'], + url='https://launchpad.net/django-openid-auth', + download_url=('http://launchpad.net/django-openid-auth/trunk/%s/+download' + '/django-openid-auth-%s.tar.gz' % (VERSION, VERSION)), + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Web Environment', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Software Development :: Libraries :: Python Modules' + ], + packages=[ + 'django_openid_auth', + 'django_openid_auth.management', + 'django_openid_auth.management.commands', + 'django_openid_auth.tests', + ], + package_data={ + 'django_openid_auth': ['templates/openid/*.html'], + }, + provides=['django_openid_auth'], + requires=['django (>=1.1.2)', 'openid (>=2.2.0)'], + ) diff --git a/desktop/core/ext-py/python-openid-2.2.5/LICENSE b/desktop/core/ext-py/python-openid-2.2.5/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/desktop/core/ext-py/python-openid-2.2.5/MANIFEST.in b/desktop/core/ext-py/python-openid-2.2.5/MANIFEST.in new file mode 100644 index 0000000..a1d314b --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/MANIFEST.in @@ -0,0 +1,7 @@ +include LICENSE NOTICE CHANGELOG MANIFEST.in NEWS background-associations.txt +graft admin +graft contrib +recursive-include examples README discover *.py *.html *.xml +recursive-include openid/test *.txt dhpriv n2b64 *.py +recursive-include openid/test/data * +recursive-include doc *.css *.html diff --git a/desktop/core/ext-py/python-openid-2.2.5/NEWS b/desktop/core/ext-py/python-openid-2.2.5/NEWS new file mode 100644 index 0000000..bd197e0 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/NEWS @@ -0,0 +1,225 @@ +What's New in Python OpenID 2.1.0 +================================= + +This implementation of OpenID has been upgraded to support version 2.0 +of the OpenID Authentication specification. + +New in this version is: + + * Verification of relying party return_to addresses, to screen out RPs + hiding behind open redirect relays. Server code can invoke this with + the returnToVerified method on CheckIDRequest. + + * Helper module for the Provider Authentication Policy Extension (PAPE) in + openid.extensions.pape. + + * Helper module for Attribute Exchange in openid.extensions.ax. + +Bugfixes: + + * Allow the use of lxml as an ElementTree implemenation. + + * Provide compatability with a wider range of versions for SQL stores. + + +Upgrading from 2.0.1 +-------------------- + +The third argument to Consumer.complete() is required. + +The sreg module should be imported from openid.extensions.sreg instead of +openid.sreg. + +The ax module should likewise be imported from openid.extensions.ax +instead of openid.ax + +The openid.extensions.ax.FetchRequest.fromOpenIDRequest method now +takes a CheckIDRequest object instead of a Message object + +The OpenID response (the result of Consumer.complete()) now has a +getDisplayIdentifier() method which should be called instead of +accessing response.identity_url. The value of getDisplayIdentifier() +will be the XRI i-name if XRI is used. The value of +response.identity_url SHOULD, however, be used as the application's +database key for storing account information. + +What's New in Python OpenID 2.0 +=============================== + +The big news here is compatibility with svn revision 313 of the OpenID 2.0 +draft specification. + +Highlights include: + + * Simple Registration support in a new module openid.sreg. (Those + previously using SuccessResponse.extensionResponse are advised to + look here.) + * OpenID provider-driven identifier selection. + * "Negotiators" allow you to define which association types to use. + * Examples for Django. + +Dependencies +------------ + +Python 2.5 is now supported. Support for Python 2.2 discontinued. +Seperate installation of yadis and urljr packages is no longer +required; they have been included in this package. + + +Upgrading from 1.1 or 1.2 +------------------------- + +One of the additions to the OpenID protocol was a specified nonce +format for one-way nonces. As a result, the nonce table in the store +has changed. You'll need to run contrib/upgrade-store-1.1-to-2.0 to +upgrade your store, or you'll encounter errors about the wrong number +of columns in the oid_nonces table. + +If you've written your own custom store or code that interacts directly with it, +you'll need to review the change notes in openid.store.interface. + +Consumers should now pass an additional parameter to Consumer.complete() +to defend against return_to tampering. + + +What's New in Python OpenID 1.1.2 +================================= + +i-name Support +-------------- + +This version of the library allows the use of XRI as OpenID identifiers, +allowing users to log in with their i-names. For full XRI compatibility, +relying parties integrating this library should take note of the user's +CanonicalID, as described in the "Identifying the End User" section of the +OpenID 2.0 specification. + +Bug Fixes +--------- + +A variety of bug fixes were included in this release, mostly relating to +international issues such as dealing with other character sets, Unicode, +incorrectly flagging certain Norwegian trust roots as suspect, and operation +of the filesystem-backed store on exotic platforms. + +Dependencies +------------ + + * urljr 1.0.1 + * yadis 1.1.0 + + +What's New in Python OpenID 1.1.0 +================================= + +Version 1.1 of the Python OpenID library implements recent changes to +the OpenID specification as well as making API changes that should +make integration with applications easier. + +Yadis Support +------------- + +One of the major changes to OpenID since the last release has been the +approval of Yadis discovery as the preferred way to specify the OpenID +metadata for an identity URL instead of using tags in +HTML. This library does Yadis discovery, and if that fails, it falls +back to old-style discovery. + +Some advantages of Yadis support are: + + * Support for fallback if your primary OpenID provider is not available + + * Support for load-balancing between OpenID servers + + * Easy interoperability for different identity services + +For more information about Yadis, see http://yadis.org/ + +Extension Support +----------------- + +OpenID also has formalized support for extensions. Extensions are a +mechanism for transferring information from the consumer to the server +and from the server to the consumer in the process of performing +OpenID authentication. Extensions are implemented as additional +namespaced query arguments that go along with standard OpenID requests +and responses. This library provides a simple API for adding extension +arguments to requests and extracting extension responses from replies. + +Dependencies +------------ + +These dependencies should be available from wherever you acquired the +OpenID library. + + * urljr - The fetcher abstraction from the previous OpenID library + has been extended and is also used for the Yadis library. Because + the Yadis library is useful without the OpenID library, the HTTP + fetching code has been rolled into its own package. Additionally, + the library now has the concept of a default fetcher to make APIs + simpler. + + * yadis - The Yadis library provides a general discovery layer that + has been adopted by OpenID as well as other identity-related + protocols. Most OpenID identity URLs will work without the Yadis + library, but as time goes on, this library will be more and more + important. + +Consumer API +------------ + +The consumer API has been changed for more natural use as well as to +support extension arguments. + + * OpenIDConsumer(store, [fetcher], [immediate]) is now + Consumer(session, store) + + - The session object is a dictionary-like object that should be + tied to the requesting HTTP agent, for example, using a session + ID cookie. It is used for Yadis fallback and holding the state + of the OpenID transaction between the redirect to the server + and the response. The values that are placed in the session are + namespaced, so there should not be a conflict with other uses + of the same session. The session namespace is an attribute of + the Consumer object. + + - Because the consumer object now does session management, it is + necessary to construct a new consumer object for every + request. Creating consumer objects is light-weight. + + * OpenIDConsumer.beginAuth(user_url) is now Consumer.begin(user_url) + and either returns an AuthRequest object or raises an + exception. There is no more tuple unpacking or status codes. + + * OpenIDConsumer.constructRedirect(authreq, return_to, trust_root) is + now AuthRequest.redirectURL(trust_root, return_to, [immediate]). + + * OpenIDConsumer.completeAuth(token, query) is now + Consumer.complete(query). It no longer returns a tuple. Instead it + returns an object that has a status code and additional information + about the response. See the API documentation for more information. + +Server API +---------- + +The server API has been changed for greater extensibility. Instead +of taking an "is_authorized" callback, processing happens in several +stages, allowing you to insert extension data into the response +before it is signed and returned. See the documentation for the +openid.server.server module. + +Fetcher API +----------- + + * fetcher was openid.consumer.fetchers.OpenIDHTTPFetcher, is now + urljr.fetchers.HTTPFetcher. get() and post() have been replaced by + fetch(), see urljr.fetchers for details. + +Upgrading from 1.0 +------------------ + +The server changed the way it indexes associations in the store, so if +you're upgrading a server installation, we recommend you clear the old +records from your store when you do so. As a consequence, consumers +will re-establish associations with your server a little sooner than +they would have otherwise. diff --git a/desktop/core/ext-py/python-openid-2.2.5/NOTICE b/desktop/core/ext-py/python-openid-2.2.5/NOTICE new file mode 100644 index 0000000..e63503e --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/NOTICE @@ -0,0 +1,4 @@ +Python OpenID may be obtained from +http://github.com/openid/python-openid +and we'd like to hear about how you're using this software. +Write to us at openid@janrain.com. diff --git a/desktop/core/ext-py/python-openid-2.2.5/PKG-INFO b/desktop/core/ext-py/python-openid-2.2.5/PKG-INFO new file mode 100644 index 0000000..a23eb0b --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/PKG-INFO @@ -0,0 +1,25 @@ +Metadata-Version: 1.0 +Name: python-openid +Version: 2.2.5 +Summary: OpenID support for servers and consumers. +Home-page: http://github.com/openid/python-openid +Author: JanRain +Author-email: openid@janrain.com +License: UNKNOWN +Download-URL: http://github.com/openid/python-openid/tarball/2.2.5 +Description: This is a set of Python packages to support use of + the OpenID decentralized identity system in your application. Want to enable + single sign-on for your web site? Use the openid.consumer package. Want to + run your own OpenID server? Check out openid.server. Includes example code + and support for a variety of storage back-ends. +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Operating System :: POSIX +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: System :: Systems Administration :: Authentication/Directory diff --git a/desktop/core/ext-py/python-openid-2.2.5/README b/desktop/core/ext-py/python-openid-2.2.5/README new file mode 100644 index 0000000..98d97e6 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/README @@ -0,0 +1,68 @@ +This is the Python OpenID library. + +REQUIREMENTS +============ + + - Python 2.3, 2.4, or 2.5. + + - ElementTree. This is included in the Python 2.5 standard library, + but users of earlier versions of Python may need to install it + seperately. + + - pycrypto, if on Python 2.3 and without /dev/urandom, or on Python + 2.3 or 2.4 and you want SHA256. + + +INSTALLATION +============ + +To install the base library, just run the following command: + +python setup.py install + +To run setup.py you need the distutils module from the Python standard +library; some distributions package this seperately in a "python-dev" +package. + + +GETTING STARTED +=============== + +The examples directory includes an example server and consumer +implementation. See the README file in that directory for more +information on running the examples. + +Library documentation is available in html form in the doc directory. + + +LOGGING +======= + +This library offers a logging hook that will record unexpected +conditions that occur in library code. If a condition is recoverable, +the library will recover and issue a log message. If it is not +recoverable, the library will raise an exception. See the +documentation for the openid.oidutil module for more on the logging +hook. + + +DOCUMENTATION +============= + +The documentation in this library is in Epydoc format, which is +detailed at: + + http://epydoc.sourceforge.net/ + + +CONTACT +======= + +Send bug reports, suggestions, comments, and questions to +http://openid.net/developers/dev-mailing-lists/. + +If you have a bugfix or feature you'd like to contribute, don't +hesitate to send it to us. For more detailed information on how to +contribute, see + + http://openidenabled.com/contribute/ diff --git a/desktop/core/ext-py/python-openid-2.2.5/admin/builddiscover.py b/desktop/core/ext-py/python-openid-2.2.5/admin/builddiscover.py new file mode 100644 index 0000000..d065c0a --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/admin/builddiscover.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +import os.path +import urlparse + +from openid.test import discoverdata + +manifest_header = """\ +# This file contains test cases for doing YADIS identity URL and +# service discovery. For each case, there are three URLs. The first +# URL is the user input. The second is the identity URL and the third +# is the URL from which the XRDS document should be read. +# +# The file format is as follows: +# User URL Identity URL XRDS URL +# +# blank lines and lines starting with # should be ignored. +# +# To use this test: +# +# 1. Run your discovery routine on the User URL. +# +# 2. Compare the identity URL returned by the discovery routine to the +# identity URL on that line of the file. It must be an EXACT match. +# +# 3. Do a regular HTTP GET on the XRDS URL. Compare the content that +# was returned by your discovery routine with the content returned +# from that URL. It should also be an exact match. + +""" + +def buildDiscover(base_url, out_dir): + """Convert all files in a directory to apache mod_asis files in + another directory.""" + test_data = discoverdata.readTests(discoverdata.default_test_file) + + def writeTestFile(test_name): + template = test_data[test_name] + + data = discoverdata.fillTemplate( + test_name, template, base_url, discoverdata.example_xrds) + + out_file_name = os.path.join(out_dir, test_name) + out_file = file(out_file_name, 'w') + out_file.write(data) + + manifest = [manifest_header] + for success, input_name, id_name, result_name in discoverdata.testlist: + if not success: + continue + writeTestFile(input_name) + + input_url = urlparse.urljoin(base_url, input_name) + id_url = urlparse.urljoin(base_url, id_name) + result_url = urlparse.urljoin(base_url, result_name) + + manifest.append('\t'.join((input_url, id_url, result_url))) + manifest.append('\n') + + manifest_file_name = os.path.join(out_dir, 'manifest.txt') + manifest_file = file(manifest_file_name, 'w') + for chunk in manifest: + manifest_file.write(chunk) + manifest_file.close() + +if __name__ == '__main__': + import sys + buildDiscover(*sys.argv[1:]) diff --git a/desktop/core/ext-py/python-openid-2.2.5/admin/fixperms b/desktop/core/ext-py/python-openid-2.2.5/admin/fixperms new file mode 100644 index 0000000..d0303e1 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/admin/fixperms @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +cat - < 60: + print output_line + line_suffix + output_line = line_prefix + tld + else: + output_line = new_output_line + prefix = separator + +print output_line + suffix diff --git a/desktop/core/ext-py/python-openid-2.2.5/admin/makechangelog b/desktop/core/ext-py/python-openid-2.2.5/admin/makechangelog new file mode 100644 index 0000000..f895b59 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/admin/makechangelog @@ -0,0 +1,2 @@ +#!/bin/bash +darcs changes --from-tag '^release-' --summary > CHANGELOG diff --git a/desktop/core/ext-py/python-openid-2.2.5/admin/makedoc b/desktop/core/ext-py/python-openid-2.2.5/admin/makedoc new file mode 100644 index 0000000..e477c19 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/admin/makedoc @@ -0,0 +1,7 @@ +#!/bin/bash + +rm -rf doc/* +epydoc --html --output doc/ --name "Python-OpenID" --top openid \ + --url "http://openidenabled.com/python-openid/" --inheritance listed \ + --no-private -v \ + $( find openid -name '*.py' -and -not -path 'openid/test*' ) diff --git a/desktop/core/ext-py/python-openid-2.2.5/admin/pythonsource b/desktop/core/ext-py/python-openid-2.2.5/admin/pythonsource new file mode 100644 index 0000000..aeddddb --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/admin/pythonsource @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +find ./openid/ \( \( -name _darcs -o -name .svn -o -name _trial_temp -o -name test -o -name admin -o -name examples \) -type d -prune -a -false \) -o \( ! -iname \*~ -a -type f \) -a \( ! -iname \*.pyc -a -type f \) -a -name \*.py diff --git a/desktop/core/ext-py/python-openid-2.2.5/admin/runtests b/desktop/core/ext-py/python-openid-2.2.5/admin/runtests new file mode 100644 index 0000000..b2a3a79 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/admin/runtests @@ -0,0 +1,204 @@ +#!/usr/bin/env python +import os.path, sys, warnings + +test_modules = [ + 'cryptutil', + 'oidutil', + 'dh', + ] + +def fixpath(): + try: + d = os.path.dirname(__file__) + except NameError: + d = os.path.dirname(sys.argv[0]) + parent = os.path.normpath(os.path.join(d, '..')) + if parent not in sys.path: + print "putting %s in sys.path" % (parent,) + sys.path.insert(0, parent) + +def otherTests(): + failed = [] + for module_name in test_modules: + print 'Testing %s...' % (module_name,) , + sys.stdout.flush() + module_name = 'openid.test.' + module_name + try: + test_mod = __import__(module_name, {}, {}, [None]) + except ImportError: + print 'Failed to import test %r' % (module_name,) + failed.append(module_name) + else: + try: + test_mod.test() + except (SystemExit, KeyboardInterrupt): + raise + except: + sys.excepthook(*sys.exc_info()) + failed.append(module_name) + else: + print 'Succeeded.' + + + return failed + +def pyunitTests(): + import unittest + pyunit_module_names = [ + 'server', + 'consumer', + 'message', + 'symbol', + 'etxrd', + 'xri', + 'xrires', + 'association_response', + 'auth_request', + 'negotiation', + 'verifydisco', + 'sreg', + 'ax', + 'pape', + 'pape_draft2', + 'pape_draft5', + 'rpverify', + 'extension', + ] + + pyunit_modules = [ + __import__('openid.test.test_%s' % (name,), {}, {}, ['unused']) + for name in pyunit_module_names + ] + + try: + from openid.test import test_examples + except ImportError, e: + if 'twill' in str(e): + warnings.warn("Could not import twill; skipping test_examples.") + else: + raise + else: + pyunit_modules.append(test_examples) + + # Some modules have data-driven tests, and they use custom methods + # to build the test suite: + custom_module_names = [ + 'kvform', + 'linkparse', + 'oidutil', + 'storetest', + 'test_accept', + 'test_association', + 'test_discover', + 'test_fetchers', + 'test_htmldiscover', + 'test_nonce', + 'test_openidyadis', + 'test_parsehtml', + 'test_urinorm', + 'test_yadis_discover', + 'trustroot', + ] + + loader = unittest.TestLoader() + s = unittest.TestSuite() + + for m in pyunit_modules: + s.addTest(loader.loadTestsFromModule(m)) + + for name in custom_module_names: + m = __import__('openid.test.%s' % (name,), {}, {}, ['unused']) + try: + s.addTest(m.pyUnitTests()) + except AttributeError, ex: + # because the AttributeError doesn't actually say which + # object it was. + print "Error loading tests from %s:" % (name,) + raise + + runner = unittest.TextTestRunner() # verbosity=2) + + return runner.run(s) + + + +def splitDir(d, count): + # in python2.4 and above, it's easier to spell this as + # d.rsplit(os.sep, count) + for i in xrange(count): + d = os.path.dirname(d) + return d + + + +def _import_djopenid(): + """Import djopenid from examples/ + + It's not in sys.path, and I don't really want to put it in sys.path. + """ + import types + thisfile = os.path.abspath(sys.modules[__name__].__file__) + topDir = splitDir(thisfile, 2) + djdir = os.path.join(topDir, 'examples', 'djopenid') + + djinit = os.path.join(djdir, '__init__.py') + + djopenid = types.ModuleType('djopenid') + execfile(djinit, djopenid.__dict__) + djopenid.__file__ = djinit + + # __path__ is the magic that makes child modules of the djopenid package + # importable. New feature in python 2.3, see PEP 302. + djopenid.__path__ = [djdir] + sys.modules['djopenid'] = djopenid + + + +def django_tests(): + """Runs tests from examples/djopenid. + + @returns: number of failed tests. + """ + import os + # Django uses this to find out where its settings are. + os.environ['DJANGO_SETTINGS_MODULE'] = 'djopenid.settings' + + _import_djopenid() + + try: + import django.test.simple + except ImportError, e: + warnings.warn("django.test.simple not found; " + "django examples not tested.") + return 0 + import djopenid.server.models, djopenid.consumer.models + print "Testing Django examples:" + + # These tests do get put in to a pyunit test suite, so we could run them + # with the other pyunit tests, but django also establishes a test database + # for them, so we let it do that thing instead. + return django.test.simple.run_tests([djopenid.server.models, + djopenid.consumer.models]) + +try: + bool +except NameError: + def bool(x): + return not not x + +def main(): + fixpath() + other_failed = otherTests() + pyunit_result = pyunitTests() + django_failures = django_tests() + + if other_failed: + print 'Failures:', ', '.join(other_failed) + + failed = (bool(other_failed) or + bool(not pyunit_result.wasSuccessful()) or + (django_failures > 0)) + return failed + +if __name__ == '__main__': + sys.exit(main() and 1 or 0) diff --git a/desktop/core/ext-py/python-openid-2.2.5/admin/setversion b/desktop/core/ext-py/python-openid-2.2.5/admin/setversion new file mode 100644 index 0000000..ea2b20c --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/admin/setversion @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +cat <&1 >/dev/null || { + echo "$VERSION" + echo "Malformed version number. Expected X.X.X or X.X.X-name." 1>&2 + exit 1 +} + +cd $(dirname $(dirname $(readlink --canonicalize "$0"))) +./admin/setversion "$VERSION" +darcs record -m 'Set version number to '"$VERSION" && darcs tag "release-$VERSION" diff --git a/desktop/core/ext-py/python-openid-2.2.5/background-associations.txt b/desktop/core/ext-py/python-openid-2.2.5/background-associations.txt new file mode 100644 index 0000000..2d8bcc7 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/background-associations.txt @@ -0,0 +1,96 @@ +Background association requests +############################### + +This document describes how to make signing in with OpenID faster for +users of your application by never making the users wait for an +association to be made, but using associations when they're +available. Most OpenID libraries and applications attempt to make +associations during the discovery phase of the OpenID authentication +request. Because association requests may have to do Diffie-Hellman +key exchange, which is time consuming. Even if Diffie-Hellman key +exchange is not used, the user still needs to wait for the association +request. + +Setting up your application to make associations in the background +================================================================== + +When making associations background, there are two components that +need access to the OpenID association store: the consumer application +and the background association fetcher. The consumer needs to be set +up to record the server URL for any request for which an association +does not exist or is expired instead of making a new association. The +background fetcher looks at the server URL queue and makes +associations for any server URLs that need them. After the +associations are made, the consumer will use them until they expire +again. While associations are expired or missing, the consumer will +use stateless mode to complete authentications with the servers that +need associations. + +The OpenID server endpoint URL queue +----------------------------------------------------------------- + +You will have to set up a conduit between the consumer and the +background association fetcher so that the background association +fetcher knows what servers need associations. The background +association fetcher will not fetch associations for servers that +already have them, so the queue does not have to be very smart. It +could be as simple as a file to which the server URLs are +appended. Either way, the queue needs to be write-able by the consumer +and readable by the background fetcher. + +Configuring the consumer +----------------------------------------------------------------- + +Create a subclass of ``GenericConsumer`` that overrides +``_negotiateAssociation`` so that it just records the server URL that +needs an association:: + + from openid.consumer.consumer import GenericConsumer, Consumer + + class LazyAssociationConsumer(GenericConsumer): + needs_assoc_file = None + + def _negotiateAssociation(self, endpoint): + # Do whatever you need to do here to send the server_url to + # the queue. This example just appends it to a file. + self.needs_assoc_file.write(endpoint.server_url + '\n') + self.needs_assoc_file.flush() + +You could also store the whole endpoint object. When you instantiate +the consumer, pass this generic consumer class to the controlling +consumer:: + + return Consumer(session, store, consumer_class=LazyAssociationConsumer) + +The background association fetcher +----------------------------------------------------------------- + +The background association fetcher is just a script that should be +added to ``cron`` or triggered periodically. If you are ambitious, you +could make the background fetcher listen for inserts into the queue. + +The background fetcher needs to do something similar to the following:: + + def refresh(consumer, endpoint): + if consumer.store.getAssociation(endpoint.server_url): + logging.info("We don't need to associate with %r", endpoint.server_url) + return + + logging.info("Associating with %r", endpoint.server_url) + now = time.time() + assoc = consumer._negotiateAssociation(endpoint) + if assoc: + elapsed = time.time() - now + logging.info('(%0.2f seconds) Associated with %r', elapsed, + endpoint.server_url) + consumer.store.storeAssociation(endpoint.server_url, assoc) + else: + logging.error('Failed to make an association with %r', + endpoint.server_url) + +The code in this example logs the amount of time that the association +request took. This is time that the user would have been waiting. The +``consumer`` in this example is a standard consumer, not the +``LazyAssociationConsumer`` that was defined in the section +above. This is important, because the lazy consumer above will not +actually make any associations. diff --git a/desktop/core/ext-py/python-openid-2.2.5/contrib/associate b/desktop/core/ext-py/python-openid-2.2.5/contrib/associate new file mode 100755 index 0000000..4cb05c3 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/contrib/associate @@ -0,0 +1,47 @@ +#!/usr/bin/env python +"""Make an OpenID Assocition request against an endpoint +and print the results.""" + +import sys + +from openid.store.memstore import MemoryStore +from openid.consumer import consumer +from openid.consumer.discover import OpenIDServiceEndpoint + +from datetime import datetime + +def verboseAssociation(assoc): + """A more verbose representation of an Association. + """ + d = assoc.__dict__ + issued_date = datetime.fromtimestamp(assoc.issued) + d['issued_iso'] = issued_date.isoformat() + fmt = """ Type: %(assoc_type)s + Handle: %(handle)s + Issued: %(issued)s [%(issued_iso)s] + Lifetime: %(lifetime)s + Secret: %(secret)r +""" + return fmt % d + +def main(): + if not sys.argv[1:]: + print "Usage: %s ENDPOINT_URL..." % (sys.argv[0],) + for endpoint_url in sys.argv[1:]: + print "Associating with", endpoint_url + + # This makes it clear why j3h made AssociationManager when we + # did the ruby port. We can't invoke requestAssociation + # without these other trappings. + store = MemoryStore() + endpoint = OpenIDServiceEndpoint() + endpoint.server_url = endpoint_url + c = consumer.GenericConsumer(store) + auth_req = c.begin(endpoint) + if auth_req.assoc: + print verboseAssociation(auth_req.assoc) + else: + print " ...no association." + +if __name__ == '__main__': + main() diff --git a/desktop/core/ext-py/python-openid-2.2.5/contrib/openid-parse b/desktop/core/ext-py/python-openid-2.2.5/contrib/openid-parse new file mode 100644 index 0000000..21ab18d --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/contrib/openid-parse @@ -0,0 +1,151 @@ +#!/usr/bin/env python +"""Grab URLs from the clipboard, interpret the queries as OpenID, and print. + +In addition to URLs, I also scan for queries as they appear in httpd log files, +with a pattern like 'GET /foo?bar=baz HTTP'. + +Requires the 'xsel' program to get the contents of the clipboard. +""" + +from pprint import pformat +from urlparse import urlsplit, urlunsplit +import cgi, re, subprocess, sys + +from openid import message + +OPENID_SORT_ORDER = ['mode', 'identity', 'claimed_id'] + +class NoQuery(Exception): + def __init__(self, url): + self.url = url + + def __str__(self): + return "No query in url %s" % (self.url,) + + +def getClipboard(): + xsel = subprocess.Popen(["xsel", "-o", "-b"], stdout=subprocess.PIPE) + output = xsel.communicate()[0] + return output + + +def main(): + source = getClipboard() + urls = find_urls(source) + + errors = [] + output = [] + queries = [] + + queries.extend(queriesFromPostdata(source)) + + for url in urls: + try: + queries.append(queryFromURL(url)) + except NoQuery, err: + errors.append(err) + + queries.extend(queriesFromLogs(source)) + + for where, query in queries: + output.append('at %s:\n%s' % (where, openidFromQuery(query))) + + if output: + print '\n\n'.join(output) + elif errors: + for err in errors: + print err + + +def queryFromURL(url): + split_url = urlsplit(url) + query = cgi.parse_qs(split_url[3]) + + if not query: + raise NoQuery(url) + + url_without_query = urlunsplit(split_url[:3] + (None, None)) + + return (url_without_query, query) + + +def openidFromQuery(query): + try: + msg = message.Message.fromPostArgs(unlistify(query)) + s = formatOpenIDMessage(msg) + except Exception, err: + # XXX - side effect. + sys.stderr.write(str(err)) + s = pformat(query) + + return s + + +def formatOpenIDMessage(msg): + value_lists = {} + for (ns_uri, ns_key), value in msg.args.items(): + l = value_lists.setdefault(ns_uri, {}) + l[ns_key] = value + + output = [] + + for ns_uri, values in value_lists.items(): + ns_output = [] + + alias = msg.namespaces.getAlias(ns_uri) + if alias is message.NULL_NAMESPACE: + alias = 'openid' + ns_output.append(" %s <%s>" % (alias, ns_uri)) + + for key in OPENID_SORT_ORDER: + try: + ns_output.append(" %s = %s" % (key, values.pop(key))) + except KeyError: + pass + + values = values.items() + values.sort() + + for k, v in values: + ns_output.append(" %s = %s" % (k, v)) + + output.append('\n'.join(ns_output)) + + return '\n\n'.join(output) + + +def unlistify(d): + return dict((i[0], i[1][0]) for i in d.items()) + + +def queriesFromLogs(s): + qre = re.compile(r'GET (/.*)?\?(.+) HTTP') + + return [(match.group(1), cgi.parse_qs(match.group(2))) + for match in qre.finditer(s)] + +def queriesFromPostdata(s): + # This looks for query data in a line that starts POSTDATA=. + # Tamperdata outputs such lines. If there's a 'Host=' in that block, + # use that too, but don't require it. + qre = re.compile(r'(?:^Host=(?P.+?)$.*?)?^POSTDATA=(?P.*)$', + re.DOTALL | re.MULTILINE) + return [(match.group('host') or 'POSTDATA', + cgi.parse_qs(match.group('query'))) for match in qre.finditer(s)] + +def find_urls(s): + # Regular expression borrowed from urlscan + # by Daniel Burrows , GPL. + urlinternalpattern=r'[{}a-zA-Z/\-_0-9%?&.=:;+,#~]' + urltrailingpattern=r'[{}a-zA-Z/\-_0-9%&=+#]' + httpurlpattern = r'(?:https?://' + urlinternalpattern + r'*' + urltrailingpattern + r')' + # Used to guess that blah.blah.blah.TLD is a URL. + tlds=['biz', 'com', 'edu', 'info', 'org'] + guessedurlpattern=r'(?:[a-zA-Z0-9_\-%]+(?:\.[a-zA-Z0-9_\-%]+)*\.(?:' + '|'.join(tlds) + '))' + urlre = re.compile(r'(?:<(?:URL:)?)?(' + httpurlpattern + '|' + guessedurlpattern + '|(?:mailto:[a-zA-Z0-9\-_]*@[0-9a-zA-Z_\-.]*[0-9a-zA-Z_\-]))>?') + + return [match.group(1) for match in urlre.finditer(s)] + + +if __name__ == '__main__': + main() diff --git a/desktop/core/ext-py/python-openid-2.2.5/contrib/upgrade-store-1.1-to-2.0 b/desktop/core/ext-py/python-openid-2.2.5/contrib/upgrade-store-1.1-to-2.0 new file mode 100644 index 0000000..1f587c3 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/contrib/upgrade-store-1.1-to-2.0 @@ -0,0 +1,170 @@ +#!/usr/bin/env python +# SQL Store Upgrade Script +# for version 1.x to 2.0 of the OpenID library. +# Doesn't depend on the openid library, so you can run this python +# script to update databases for ruby or PHP as well. +# +# Testers note: +# +# A SQLite3 db with the 1.2 schema exists in +# openid/test/data/openid-1.2-consumer-sqlitestore.db if you want something +# to try upgrading. +# +# TODO: +# * test data for mysql and postgresql. +# * automated tests. + +import os +import getpass +import sys +from optparse import OptionParser + + +def askForPassword(): + return getpass.getpass("DB Password: ") + +def askForConfirmation(dbname,tablename): + print """The table %s from the database %s will be dropped, and + an empty table with the new nonce table schema will replace it."""%( + tablename, dbname) + return raw_input("Continue? ").lower().strip().startswith('y') + +def doSQLiteUpgrade(db_conn, nonce_table_name='oid_nonces'): + cur = db_conn.cursor() + cur.execute('DROP TABLE %s'%nonce_table_name) + sql = """ + CREATE TABLE %s ( + server_url VARCHAR, + timestamp INTEGER, + salt CHAR(40), + UNIQUE(server_url, timestamp, salt) + ); + """%nonce_table_name + cur.execute(sql) + cur.close() + +def doMySQLUpgrade(db_conn, nonce_table_name='oid_nonces'): + cur = db_conn.cursor() + cur.execute('DROP TABLE %s'%nonce_table_name) + sql = """ + CREATE TABLE %s ( + server_url BLOB, + timestamp INTEGER, + salt CHAR(40), + PRIMARY KEY (server_url(255), timestamp, salt) + ) + TYPE=InnoDB; + """%nonce_table_name + cur.execute(sql) + cur.close() + +def doPostgreSQLUpgrade(db_conn, nonce_table_name='oid_nonces'): + cur = db_conn.cursor() + cur.execute('DROP TABLE %s'%nonce_table_name) + sql = """ + CREATE TABLE %s ( + server_url VARCHAR(2047), + timestamp INTEGER, + salt CHAR(40), + PRIMARY KEY (server_url, timestamp, salt) + ); + """%nonce_table_name + cur.execute(sql) + cur.close() + db_conn.commit() + +def main(argv=None): + parser = OptionParser() + parser.add_option("-u", "--user", dest="username", + default=os.environ.get('USER'), + help="User name to use to connect to the DB. " + "Defaults to USER environment variable.") + parser.add_option('-t', '--table', dest='tablename', default='oid_nonces', + help='The name of the nonce table to drop and recreate. ' + ' Defaults to "oid_nonces", the default table name for ' + 'the openid stores.') + parser.add_option('--mysql', dest='mysql_db_name', + help='Upgrade a table from this MySQL database. ' + 'Requires username for database.') + parser.add_option('--pg', '--postgresql', dest='postgres_db_name', + help='Upgrade a table from this PostgreSQL database. ' + 'Requires username for database.') + parser.add_option('--sqlite', dest='sqlite_db_name', + help='Upgrade a table from this SQLite database file.') + parser.add_option('--host', dest='db_host', + default='localhost', + help='Host on which to find MySQL or PostgreSQL DB.') + (options, args) = parser.parse_args(argv) + + db_conn = None + + if options.sqlite_db_name: + try: + from pysqlite2 import dbapi2 as sqlite + except ImportError: + print "You must have pysqlite2 installed in your PYTHONPATH." + return 1 + try: + db_conn = sqlite.connect(options.sqlite_db_name) + except Exception, e: + print "Could not connect to SQLite database:", str(e) + return 1 + + if askForConfirmation(options.sqlite_db_name, options.tablename): + doSQLiteUpgrade(db_conn, nonce_table_name=options.tablename) + + if options.postgres_db_name: + if not options.username: + print "A username is required to open a PostgreSQL Database." + return 1 + password = askForPassword() + try: + import psycopg + except ImportError: + print "You need psycopg installed to update a postgres DB." + return 1 + + try: + db_conn = psycopg.connect(database = options.postgres_db_name, + user = options.username, + host = options.db_host, + password = password) + except Exception, e: + print "Could not connect to PostgreSQL database:", str(e) + return 1 + + if askForConfirmation(options.postgres_db_name, options.tablename): + doPostgreSQLUpgrade(db_conn, nonce_table_name=options.tablename) + + if options.mysql_db_name: + if not options.username: + print "A username is required to open a MySQL Database." + return 1 + password = askForPassword() + try: + import MySQLdb + except ImportError: + print "You must have MySQLdb installed to update a MySQL DB." + return 1 + + try: + db_conn = MySQLdb.connect(options.db_host, options.username, + password, options.mysql_db_name) + except Exception, e: + print "Could not connect to MySQL database:", str(e) + return 1 + + if askForConfirmation(options.mysql_db_name, options.tablename): + doMySQLUpgrade(db_conn, nonce_table_name=options.tablename) + + if db_conn: + db_conn.close() + else: + parser.print_help() + + return 0 + + +if __name__ == '__main__': + retval = main() + sys.exit(retval) diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/README b/desktop/core/ext-py/python-openid-2.2.5/examples/README new file mode 100644 index 0000000..87c0831 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/README @@ -0,0 +1,91 @@ +Python OpenID library example code +================================== + +The examples directory contains working code illustrating the use of +the library for performing OpenID authentication, both as a consumer +and a server. There are two kinds of examples, one that can run +without any external dependencies, and one that uses the Django Web +framework. The examples do not illustrate how to use all of the +features of the library, but they should be a good starting point to +see how to use this library with your code. + +Both the Django libraries and the BaseHTTPServer examples require that +the OpenID library is installed or that it has been added to Python's +search path (PYTHONPATH environment variable or sys.path). + +The Django example is probably a good place to start reading the +code. There is little that is Django-specific about the OpenID logic +in the example, and it should be easy to port to any framework. To run +the django examples, see the README file in the djopenid subdirectory. + +The other examples use Python's built-in BaseHTTPServer and have a +good deal of ad-hoc dispatching and rendering code mixed in + +Using the BaseHTTPServer examples +================================= + +This directory contains a working server and consumer that use this +OpenID library. They are both written using python's standard +BaseHTTPServer. + + +To run the example system: + +1. Make sure you've installed the library, as explained in the + installation instructions. + +2. Start the consumer server: + + python consumer.py --port 8001 + + +3. In another terminal, start the identity server: + + python server.py --port 8000 + + (Hit Ctrl-C in either server's window to stop that server.) + + +4. Open your web broswer, and go to the consumer server: + + http://localhost:8001/ + + Note that all pages the consumer server shows will have "Python OpenID + Consumer Example" across the top. + + +5. Enter an identity url managed by the sample identity server: + + http://localhost:8000/id/bob + + +6. The browser will be redirected to the sample server, which will be + requesting that you log in to proceed. Enter the username for the + identity URL into the login box: + + bob + + Note that all pages the identity server shows will have "Python + OpenID Server Example" across the top. + + +7. After you log in as bob, the server example will ask you if you + want to allow http://localhost:8001/ to know your identity. Say + yes. + + +8. You should end up back on the consumer site, at a page indicating + you've logged in successfully. + + +That's a basic OpenID login procedure. You can continue through it, +playing with variations to see how they work. The python code is +intended to be a straightforward example of how to use the python +OpenID library to function as either an identity server or consumer. + +Getting help +============ + +Please send bug reports, patches, and other feedback to + + http://openid.net/developers/dev-mailing-lists/ diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/__init__.py b/desktop/core/ext-py/python-openid-2.2.5/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/consumer.py b/desktop/core/ext-py/python-openid-2.2.5/examples/consumer.py new file mode 100644 index 0000000..1c38a62 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/consumer.py @@ -0,0 +1,507 @@ +#!/usr/bin/env python +""" +Simple example for an OpenID consumer. + +Once you understand this example you'll know the basics of OpenID +and using the Python OpenID library. You can then move on to more +robust examples, and integrating OpenID into your application. +""" +__copyright__ = 'Copyright 2005-2008, Janrain, Inc.' + +from Cookie import SimpleCookie +import cgi +import urlparse +import cgitb +import sys + +def quoteattr(s): + qs = cgi.escape(s, 1) + return '"%s"' % (qs,) + +from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler + +try: + import openid +except ImportError: + sys.stderr.write(""" +Failed to import the OpenID library. In order to use this example, you +must either install the library (see INSTALL in the root of the +distribution) or else add the library to python's import path (the +PYTHONPATH environment variable). + +For more information, see the README in the root of the library +distribution.""") + sys.exit(1) + +from openid.store import memstore +from openid.store import filestore +from openid.consumer import consumer +from openid.oidutil import appendArgs +from openid.cryptutil import randomString +from openid.fetchers import setDefaultFetcher, Urllib2Fetcher +from openid.extensions import pape, sreg + +# Used with an OpenID provider affiliate program. +OPENID_PROVIDER_NAME = 'MyOpenID' +OPENID_PROVIDER_URL ='https://www.myopenid.com/affiliate_signup?affiliate_id=39' + + +class OpenIDHTTPServer(HTTPServer): + """http server that contains a reference to an OpenID consumer and + knows its base URL. + """ + def __init__(self, store, *args, **kwargs): + HTTPServer.__init__(self, *args, **kwargs) + self.sessions = {} + self.store = store + + if self.server_port != 80: + self.base_url = ('http://%s:%s/' % + (self.server_name, self.server_port)) + else: + self.base_url = 'http://%s/' % (self.server_name,) + +class OpenIDRequestHandler(BaseHTTPRequestHandler): + """Request handler that knows how to verify an OpenID identity.""" + SESSION_COOKIE_NAME = 'pyoidconsexsid' + + session = None + + def getConsumer(self, stateless=False): + if stateless: + store = None + else: + store = self.server.store + return consumer.Consumer(self.getSession(), store) + + def getSession(self): + """Return the existing session or a new session""" + if self.session is not None: + return self.session + + # Get value of cookie header that was sent + cookie_str = self.headers.get('Cookie') + if cookie_str: + cookie_obj = SimpleCookie(cookie_str) + sid_morsel = cookie_obj.get(self.SESSION_COOKIE_NAME, None) + if sid_morsel is not None: + sid = sid_morsel.value + else: + sid = None + else: + sid = None + + # If a session id was not set, create a new one + if sid is None: + sid = randomString(16, '0123456789abcdef') + session = None + else: + session = self.server.sessions.get(sid) + + # If no session exists for this session ID, create one + if session is None: + session = self.server.sessions[sid] = {} + + session['id'] = sid + self.session = session + return session + + def setSessionCookie(self): + sid = self.getSession()['id'] + session_cookie = '%s=%s;' % (self.SESSION_COOKIE_NAME, sid) + self.send_header('Set-Cookie', session_cookie) + + def do_GET(self): + """Dispatching logic. There are three paths defined: + + / - Display an empty form asking for an identity URL to + verify + /verify - Handle form submission, initiating OpenID verification + /process - Handle a redirect from an OpenID server + + Any other path gets a 404 response. This function also parses + the query parameters. + + If an exception occurs in this function, a traceback is + written to the requesting browser. + """ + try: + self.parsed_uri = urlparse.urlparse(self.path) + self.query = {} + for k, v in cgi.parse_qsl(self.parsed_uri[4]): + self.query[k] = v.decode('utf-8') + + path = self.parsed_uri[2] + if path == '/': + self.render() + elif path == '/verify': + self.doVerify() + elif path == '/process': + self.doProcess() + elif path == '/affiliate': + self.doAffiliate() + else: + self.notFound() + + except (KeyboardInterrupt, SystemExit): + raise + except: + self.send_response(500) + self.send_header('Content-type', 'text/html') + self.setSessionCookie() + self.end_headers() + self.wfile.write(cgitb.html(sys.exc_info(), context=10)) + + def doVerify(self): + """Process the form submission, initating OpenID verification. + """ + + # First, make sure that the user entered something + openid_url = self.query.get('openid_identifier') + if not openid_url: + self.render('Enter an OpenID Identifier to verify.', + css_class='error', form_contents=openid_url) + return + + immediate = 'immediate' in self.query + use_sreg = 'use_sreg' in self.query + use_pape = 'use_pape' in self.query + use_stateless = 'use_stateless' in self.query + + oidconsumer = self.getConsumer(stateless = use_stateless) + try: + request = oidconsumer.begin(openid_url) + except consumer.DiscoveryFailure, exc: + fetch_error_string = 'Error in discovery: %s' % ( + cgi.escape(str(exc[0]))) + self.render(fetch_error_string, + css_class='error', + form_contents=openid_url) + else: + if request is None: + msg = 'No OpenID services found for %s' % ( + cgi.escape(openid_url),) + self.render(msg, css_class='error', form_contents=openid_url) + else: + # Then, ask the library to begin the authorization. + # Here we find out the identity server that will verify the + # user's identity, and get a token that allows us to + # communicate securely with the identity server. + if use_sreg: + self.requestRegistrationData(request) + + if use_pape: + self.requestPAPEDetails(request) + + trust_root = self.server.base_url + return_to = self.buildURL('process') + if request.shouldSendRedirect(): + redirect_url = request.redirectURL( + trust_root, return_to, immediate=immediate) + self.send_response(302) + self.send_header('Location', redirect_url) + self.writeUserHeader() + self.end_headers() + else: + form_html = request.htmlMarkup( + trust_root, return_to, + form_tag_attrs={'id':'openid_message'}, + immediate=immediate) + + self.wfile.write(form_html) + + def requestRegistrationData(self, request): + sreg_request = sreg.SRegRequest( + required=['nickname'], optional=['fullname', 'email']) + request.addExtension(sreg_request) + + def requestPAPEDetails(self, request): + pape_request = pape.Request([pape.AUTH_PHISHING_RESISTANT]) + request.addExtension(pape_request) + + def doProcess(self): + """Handle the redirect from the OpenID server. + """ + oidconsumer = self.getConsumer() + + # Ask the library to check the response that the server sent + # us. Status is a code indicating the response type. info is + # either None or a string containing more information about + # the return type. + url = 'http://'+self.headers.get('Host')+self.path + info = oidconsumer.complete(self.query, url) + + sreg_resp = None + pape_resp = None + css_class = 'error' + display_identifier = info.getDisplayIdentifier() + + if info.status == consumer.FAILURE and display_identifier: + # In the case of failure, if info is non-None, it is the + # URL that we were verifying. We include it in the error + # message to help the user figure out what happened. + fmt = "Verification of %s failed: %s" + message = fmt % (cgi.escape(display_identifier), + info.message) + elif info.status == consumer.SUCCESS: + # Success means that the transaction completed without + # error. If info is None, it means that the user cancelled + # the verification. + css_class = 'alert' + + # This is a successful verification attempt. If this + # was a real application, we would do our login, + # comment posting, etc. here. + fmt = "You have successfully verified %s as your identity." + message = fmt % (cgi.escape(display_identifier),) + sreg_resp = sreg.SRegResponse.fromSuccessResponse(info) + pape_resp = pape.Response.fromSuccessResponse(info) + if info.endpoint.canonicalID: + # You should authorize i-name users by their canonicalID, + # rather than their more human-friendly identifiers. That + # way their account with you is not compromised if their + # i-name registration expires and is bought by someone else. + message += (" This is an i-name, and its persistent ID is %s" + % (cgi.escape(info.endpoint.canonicalID),)) + elif info.status == consumer.CANCEL: + # cancelled + message = 'Verification cancelled' + elif info.status == consumer.SETUP_NEEDED: + if info.setup_url: + message = 'Setup needed' % ( + quoteattr(info.setup_url),) + else: + # This means auth didn't succeed, but you're welcome to try + # non-immediate mode. + message = 'Setup needed' + else: + # Either we don't understand the code or there is no + # openid_url included with the error. Give a generic + # failure message. The library should supply debug + # information in a log. + message = 'Verification failed.' + + self.render(message, css_class, display_identifier, + sreg_data=sreg_resp, pape_data=pape_resp) + + def doAffiliate(self): + """Direct the user sign up with an affiliate OpenID provider.""" + sreg_req = sreg.SRegRequest(['nickname'], ['fullname', 'email']) + href = sreg_req.toMessage().toURL(OPENID_PROVIDER_URL) + + message = """Get an OpenID at %s""" % ( + quoteattr(href), OPENID_PROVIDER_NAME) + self.render(message) + + def renderSREG(self, sreg_data): + if not sreg_data: + self.wfile.write( + '
No registration data was returned
') + else: + sreg_list = sreg_data.items() + sreg_list.sort() + self.wfile.write( + '

Registration Data

' + '' + '' + '') + + odd = ' class="odd"' + for k, v in sreg_list: + field_name = sreg.data_fields.get(k, k) + value = cgi.escape(v.encode('UTF-8')) + self.wfile.write( + '' % (odd, field_name, value)) + if odd: + odd = '' + else: + odd = ' class="odd"' + + self.wfile.write('
FieldValue
%s%s
') + + def renderPAPE(self, pape_data): + if not pape_data: + self.wfile.write( + '
No PAPE data was returned
') + else: + self.wfile.write('
Effective Auth Policies
    ') + + for policy_uri in pape_data.auth_policies: + self.wfile.write('
  • %s
  • ' % (cgi.escape(policy_uri),)) + + if not pape_data.auth_policies: + self.wfile.write('
  • No policies were applied.
  • ') + + self.wfile.write('
') + + def buildURL(self, action, **query): + """Build a URL relative to the server base_url, with the given + query parameters added.""" + base = urlparse.urljoin(self.server.base_url, action) + return appendArgs(base, query) + + def notFound(self): + """Render a page with a 404 return code and a message.""" + fmt = 'The path %s was not understood by this server.' + msg = fmt % (self.path,) + openid_url = self.query.get('openid_identifier') + self.render(msg, 'error', openid_url, status=404) + + def render(self, message=None, css_class='alert', form_contents=None, + status=200, title="Python OpenID Consumer Example", + sreg_data=None, pape_data=None): + """Render a page.""" + self.send_response(status) + self.pageHeader(title) + if message: + self.wfile.write("
" % (css_class,)) + self.wfile.write(message) + self.wfile.write("
") + + if sreg_data is not None: + self.renderSREG(sreg_data) + + if pape_data is not None: + self.renderPAPE(pape_data) + + self.pageFooter(form_contents) + + def pageHeader(self, title): + """Render the page header""" + self.setSessionCookie() + self.wfile.write('''\ +Content-type: text/html; charset=UTF-8 + + + %s + + +

%s

+

+ This example consumer uses the Python + OpenID library. It just verifies that the identifier that you enter + is your identifier. +

+''' % (title, title)) + + def pageFooter(self, form_contents): + """Render the page footer""" + if not form_contents: + form_contents = '' + + self.wfile.write('''\ +
+
+ Identifier: + +
+ + + + +
+
+ + +''' % (quoteattr(self.buildURL('verify')), quoteattr(form_contents))) + +def main(host, port, data_path, weak_ssl=False): + # Instantiate OpenID consumer store and OpenID consumer. If you + # were connecting to a database, you would create the database + # connection and instantiate an appropriate store here. + if data_path: + store = filestore.FileOpenIDStore(data_path) + else: + store = memstore.MemoryStore() + + if weak_ssl: + setDefaultFetcher(Urllib2Fetcher()) + + addr = (host, port) + server = OpenIDHTTPServer(store, addr, OpenIDRequestHandler) + + print 'Server running at:' + print server.base_url + server.serve_forever() + +if __name__ == '__main__': + host = 'localhost' + port = 8001 + weak_ssl = False + + try: + import optparse + except ImportError: + pass # Use defaults (for Python 2.2) + else: + parser = optparse.OptionParser('Usage:\n %prog [options]') + parser.add_option( + '-d', '--data-path', dest='data_path', + help='Data directory for storing OpenID consumer state. ' + 'Setting this option implies using a "FileStore."') + parser.add_option( + '-p', '--port', dest='port', type='int', default=port, + help='Port on which to listen for HTTP requests. ' + 'Defaults to port %default.') + parser.add_option( + '-s', '--host', dest='host', default=host, + help='Host on which to listen for HTTP requests. ' + 'Also used for generating URLs. Defaults to %default.') + parser.add_option( + '-w', '--weakssl', dest='weakssl', default=False, + action='store_true', help='Skip ssl cert verification') + + options, args = parser.parse_args() + if args: + parser.error('Expected no arguments. Got %r' % args) + + host = options.host + port = options.port + data_path = options.data_path + weak_ssl = options.weakssl + + main(host, port, data_path, weak_ssl) diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/discover b/desktop/core/ext-py/python-openid-2.2.5/examples/discover new file mode 100644 index 0000000..9b74e8a --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/discover @@ -0,0 +1,46 @@ +#!/usr/bin/env python +from openid.consumer.discover import discover, DiscoveryFailure +from openid.fetchers import HTTPFetchingError + +names = [["server_url", "Server URL "], + ["local_id", "Local ID "], + ["canonicalID", "Canonical ID"], + ] + +def show_services(user_input, normalized, services): + print " Claimed identifier:", normalized + if services: + print " Discovered OpenID services:" + for n, service in enumerate(services): + print " %s." % (n,) + for attr, name in names: + val = getattr(service, attr, None) + if val is not None: + print " %s: %s" % (name, val) + + print " Type URIs:" + for type_uri in service.type_uris: + print " *", type_uri + + print + + else: + print " No OpenID services found" + print + +if __name__ == "__main__": + import sys + + for user_input in sys.argv[1:]: + print "=" * 50 + print "Running discovery on", user_input + try: + normalized, services = discover(user_input) + except DiscoveryFailure, why: + print "Discovery failed:", why + print + except HTTPFetchingError, why: + print "HTTP request failed:", why + print + else: + show_services(user_input, normalized, services) diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/README b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/README new file mode 100644 index 0000000..e803648 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/README @@ -0,0 +1,67 @@ + +DJANGO EXAMPLE PACKAGE +====================== + +This package implements an example consumer and server for the Django +Python web framework. You can get Django (and learn more about it) at + + http://www.djangoproject.com/ + +SETUP +===== + + 1. Install the OpenID library, version 2.0.0 or later. + + 2. Install Django 0.95.1. + + If you find that the examples run on even newer versions of + Django, please let us know! + + 3. Modify djopenid/settings.py appropriately; you may wish to change + the database type or path, although the default settings should be + sufficient for most systems. + + 4. In examples/djopenid/ run: + + python manage.py syncdb + + 5. To run the example consumer or server, run + + python manage.py runserver PORT + + where PORT is the port number on which to listen. + + Note that if you want to try both the consumer and server at the + same time, run the command twice with two different values for + PORT. + + 6. Point your web browser at the server at + + http://localhost:PORT/ + + to begin. + +ABOUT THE CODE +============== + +The example server and consumer code provided in this package are +intended to be instructional in the use of this OpenID library. While +it is not recommended to use the example code in production, the code +should be sufficient to explain the general use of the library. + +If you aren't familiar with the Django web framework, you can quickly +start looking at the important code by looking in the 'views' modules: + + djopenid.consumer.views + djopenid.server.views + +Each view is a python callable that responds to an HTTP request. +Regardless of whether you use a framework, your application should +look similar to these example applications. + +CONTACT +======= + +Please send bug reports, patches, and other feedback to + + http://openid.net/developers/dev-mailing-lists/ diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/__init__.py b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/__init__.py b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/models.py b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/urls.py b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/urls.py new file mode 100644 index 0000000..d55e056 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/urls.py @@ -0,0 +1,9 @@ + +from django.conf.urls.defaults import * + +urlpatterns = patterns( + 'djopenid.consumer.views', + (r'^$', 'startOpenID'), + (r'^finish/$', 'finishOpenID'), + (r'^xrds/$', 'rpXRDS'), +) diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/views.py b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/views.py new file mode 100644 index 0000000..c899294 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/consumer/views.py @@ -0,0 +1,220 @@ + +from django import http +from django.http import HttpResponseRedirect +from django.views.generic.simple import direct_to_template + +from openid.consumer import consumer +from openid.consumer.discover import DiscoveryFailure +from openid.extensions import ax, pape, sreg +from openid.yadis.constants import YADIS_HEADER_NAME, YADIS_CONTENT_TYPE +from openid.server.trustroot import RP_RETURN_TO_URL_TYPE + +from djopenid import util + +PAPE_POLICIES = [ + 'AUTH_PHISHING_RESISTANT', + 'AUTH_MULTI_FACTOR', + 'AUTH_MULTI_FACTOR_PHYSICAL', + ] + +# List of (name, uri) for use in generating the request form. +POLICY_PAIRS = [(p, getattr(pape, p)) + for p in PAPE_POLICIES] + +def getOpenIDStore(): + """ + Return an OpenID store object fit for the currently-chosen + database backend, if any. + """ + return util.getOpenIDStore('/tmp/djopenid_c_store', 'c_') + +def getConsumer(request): + """ + Get a Consumer object to perform OpenID authentication. + """ + return consumer.Consumer(request.session, getOpenIDStore()) + +def renderIndexPage(request, **template_args): + template_args['consumer_url'] = util.getViewURL(request, startOpenID) + template_args['pape_policies'] = POLICY_PAIRS + + response = direct_to_template( + request, 'consumer/index.html', template_args) + response[YADIS_HEADER_NAME] = util.getViewURL(request, rpXRDS) + return response + +def startOpenID(request): + """ + Start the OpenID authentication process. Renders an + authentication form and accepts its POST. + + * Renders an error message if OpenID cannot be initiated + + * Requests some Simple Registration data using the OpenID + library's Simple Registration machinery + + * Generates the appropriate trust root and return URL values for + this application (tweak where appropriate) + + * Generates the appropriate redirect based on the OpenID protocol + version. + """ + if request.POST: + # Start OpenID authentication. + openid_url = request.POST['openid_identifier'] + c = getConsumer(request) + error = None + + try: + auth_request = c.begin(openid_url) + except DiscoveryFailure, e: + # Some other protocol-level failure occurred. + error = "OpenID discovery error: %s" % (str(e),) + + if error: + # Render the page with an error. + return renderIndexPage(request, error=error) + + # Add Simple Registration request information. Some fields + # are optional, some are required. It's possible that the + # server doesn't support sreg or won't return any of the + # fields. + sreg_request = sreg.SRegRequest(optional=['email', 'nickname'], + required=['dob']) + auth_request.addExtension(sreg_request) + + # Add Attribute Exchange request information. + ax_request = ax.FetchRequest() + # XXX - uses myOpenID-compatible schema values, which are + # not those listed at axschema.org. + ax_request.add( + ax.AttrInfo('http://schema.openid.net/namePerson', + required=True)) + ax_request.add( + ax.AttrInfo('http://schema.openid.net/contact/web/default', + required=False, count=ax.UNLIMITED_VALUES)) + auth_request.addExtension(ax_request) + + # Add PAPE request information. We'll ask for + # phishing-resistant auth and display any policies we get in + # the response. + requested_policies = [] + policy_prefix = 'policy_' + for k, v in request.POST.iteritems(): + if k.startswith(policy_prefix): + policy_attr = k[len(policy_prefix):] + if policy_attr in PAPE_POLICIES: + requested_policies.append(getattr(pape, policy_attr)) + + if requested_policies: + pape_request = pape.Request(requested_policies) + auth_request.addExtension(pape_request) + + # Compute the trust root and return URL values to build the + # redirect information. + trust_root = util.getViewURL(request, startOpenID) + return_to = util.getViewURL(request, finishOpenID) + + # Send the browser to the server either by sending a redirect + # URL or by generating a POST form. + if auth_request.shouldSendRedirect(): + url = auth_request.redirectURL(trust_root, return_to) + return HttpResponseRedirect(url) + else: + # Beware: this renders a template whose content is a form + # and some javascript to submit it upon page load. Non-JS + # users will have to click the form submit button to + # initiate OpenID authentication. + form_id = 'openid_message' + form_html = auth_request.formMarkup(trust_root, return_to, + False, {'id': form_id}) + return direct_to_template( + request, 'consumer/request_form.html', {'html': form_html}) + + return renderIndexPage(request) + +def finishOpenID(request): + """ + Finish the OpenID authentication process. Invoke the OpenID + library with the response from the OpenID server and render a page + detailing the result. + """ + result = {} + + # Because the object containing the query parameters is a + # MultiValueDict and the OpenID library doesn't allow that, we'll + # convert it to a normal dict. + + # OpenID 2 can send arguments as either POST body or GET query + # parameters. + request_args = util.normalDict(request.GET) + if request.method == 'POST': + request_args.update(util.normalDict(request.POST)) + + if request_args: + c = getConsumer(request) + + # Get a response object indicating the result of the OpenID + # protocol. + return_to = util.getViewURL(request, finishOpenID) + response = c.complete(request_args, return_to) + + # Get a Simple Registration response object if response + # information was included in the OpenID response. + sreg_response = {} + ax_items = {} + if response.status == consumer.SUCCESS: + sreg_response = sreg.SRegResponse.fromSuccessResponse(response) + + ax_response = ax.FetchResponse.fromSuccessResponse(response) + if ax_response: + ax_items = { + 'fullname': ax_response.get( + 'http://schema.openid.net/namePerson'), + 'web': ax_response.get( + 'http://schema.openid.net/contact/web/default'), + } + + # Get a PAPE response object if response information was + # included in the OpenID response. + pape_response = None + if response.status == consumer.SUCCESS: + pape_response = pape.Response.fromSuccessResponse(response) + + if not pape_response.auth_policies: + pape_response = None + + # Map different consumer status codes to template contexts. + results = { + consumer.CANCEL: + {'message': 'OpenID authentication cancelled.'}, + + consumer.FAILURE: + {'error': 'OpenID authentication failed.'}, + + consumer.SUCCESS: + {'url': response.getDisplayIdentifier(), + 'sreg': sreg_response and sreg_response.items(), + 'ax': ax_items.items(), + 'pape': pape_response} + } + + result = results[response.status] + + if isinstance(response, consumer.FailureResponse): + # In a real application, this information should be + # written to a log for debugging/tracking OpenID + # authentication failures. In general, the messages are + # not user-friendly, but intended for developers. + result['failure_reason'] = response.message + + return renderIndexPage(request, **result) + +def rpXRDS(request): + """ + Return a relying party verification XRDS document + """ + return util.renderXRDS( + request, + [RP_RETURN_TO_URL_TYPE], + [util.getViewURL(request, finishOpenID)]) diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/manage.py b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/manage.py new file mode 100644 index 0000000..5e78ea9 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/manage.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +from django.core.management import execute_manager +try: + import settings # Assumed to be in the same directory. +except ImportError: + import sys + sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) + sys.exit(1) + +if __name__ == "__main__": + execute_manager(settings) diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/__init__.py b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/models.py b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/tests.py b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/tests.py new file mode 100644 index 0000000..e7ddd06 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/tests.py @@ -0,0 +1,103 @@ + +from django.test.testcases import TestCase +from djopenid.server import views +from djopenid import util + +from django.http import HttpRequest +from django.contrib.sessions.middleware import SessionWrapper + +from openid.server.server import CheckIDRequest +from openid.message import Message +from openid.yadis.constants import YADIS_CONTENT_TYPE +from openid.yadis.services import applyFilter + +def dummyRequest(): + request = HttpRequest() + request.session = SessionWrapper("test") + request.META['HTTP_HOST'] = 'example.invalid' + request.META['SERVER_PROTOCOL'] = 'HTTP' + return request + +class TestProcessTrustResult(TestCase): + def setUp(self): + self.request = dummyRequest() + + id_url = util.getViewURL(self.request, views.idPage) + + # Set up the OpenID request we're responding to. + op_endpoint = 'http://127.0.0.1:8080/endpoint' + message = Message.fromPostArgs({ + 'openid.mode': 'checkid_setup', + 'openid.identity': id_url, + 'openid.return_to': 'http://127.0.0.1/%s' % (self.id(),), + 'openid.sreg.required': 'postcode', + }) + self.openid_request = CheckIDRequest.fromMessage(message, op_endpoint) + + views.setRequest(self.request, self.openid_request) + + + def test_allow(self): + self.request.POST['allow'] = 'Yes' + + response = views.processTrustResult(self.request) + + self.failUnlessEqual(response.status_code, 302) + finalURL = response['location'] + self.failUnless('openid.mode=id_res' in finalURL, finalURL) + self.failUnless('openid.identity=' in finalURL, finalURL) + self.failUnless('openid.sreg.postcode=12345' in finalURL, finalURL) + + def test_cancel(self): + self.request.POST['cancel'] = 'Yes' + + response = views.processTrustResult(self.request) + + self.failUnlessEqual(response.status_code, 302) + finalURL = response['location'] + self.failUnless('openid.mode=cancel' in finalURL, finalURL) + self.failIf('openid.identity=' in finalURL, finalURL) + self.failIf('openid.sreg.postcode=12345' in finalURL, finalURL) + + + +class TestShowDecidePage(TestCase): + def test_unreachableRealm(self): + self.request = dummyRequest() + + id_url = util.getViewURL(self.request, views.idPage) + + # Set up the OpenID request we're responding to. + op_endpoint = 'http://127.0.0.1:8080/endpoint' + message = Message.fromPostArgs({ + 'openid.mode': 'checkid_setup', + 'openid.identity': id_url, + 'openid.return_to': 'http://unreachable.invalid/%s' % (self.id(),), + 'openid.sreg.required': 'postcode', + }) + self.openid_request = CheckIDRequest.fromMessage(message, op_endpoint) + + views.setRequest(self.request, self.openid_request) + + response = views.showDecidePage(self.request, self.openid_request) + self.failUnless('trust_root_valid is Unreachable' in response.content, + response) + + + +class TestGenericXRDS(TestCase): + def test_genericRender(self): + """Render an XRDS document with a single type URI and a single endpoint URL + Parse it to see that it matches.""" + request = dummyRequest() + + type_uris = ['A_TYPE'] + endpoint_url = 'A_URL' + response = util.renderXRDS(request, type_uris, [endpoint_url]) + + requested_url = 'http://requested.invalid/' + (endpoint,) = applyFilter(requested_url, response.content) + + self.failUnlessEqual(YADIS_CONTENT_TYPE, response['Content-Type']) + self.failUnlessEqual(type_uris, endpoint.type_uris) + self.failUnlessEqual(endpoint_url, endpoint.uri) diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/urls.py b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/urls.py new file mode 100644 index 0000000..d6931a4 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/urls.py @@ -0,0 +1,12 @@ + +from django.conf.urls.defaults import * + +urlpatterns = patterns( + 'djopenid.server.views', + (r'^$', 'server'), + (r'^xrds/$', 'idpXrds'), + (r'^processTrustResult/$', 'processTrustResult'), + (r'^user/$', 'idPage'), + (r'^endpoint/$', 'endpoint'), + (r'^trust/$', 'trustPage'), +) diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/views.py b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/views.py new file mode 100644 index 0000000..67fa00b --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/server/views.py @@ -0,0 +1,279 @@ + +""" +This module implements an example server for the OpenID library. Some +functionality has been omitted intentionally; this code is intended to +be instructive on the use of this library. This server does not +perform actual user authentication and serves up only one OpenID URL, +with the exception of IDP-generated identifiers. + +Some code conventions used here: + +* 'request' is a Django request object. + +* 'openid_request' is an OpenID library request object. + +* 'openid_response' is an OpenID library response +""" + +import cgi + +from djopenid import util +from djopenid.util import getViewURL + +from django import http +from django.views.generic.simple import direct_to_template + +from openid.server.server import Server, ProtocolError, CheckIDRequest, \ + EncodingError +from openid.server.trustroot import verifyReturnTo +from openid.yadis.discover import DiscoveryFailure +from openid.consumer.discover import OPENID_IDP_2_0_TYPE +from openid.extensions import sreg +from openid.extensions import pape +from openid.fetchers import HTTPFetchingError + +def getOpenIDStore(): + """ + Return an OpenID store object fit for the currently-chosen + database backend, if any. + """ + return util.getOpenIDStore('/tmp/djopenid_s_store', 's_') + +def getServer(request): + """ + Get a Server object to perform OpenID authentication. + """ + return Server(getOpenIDStore(), getViewURL(request, endpoint)) + +def setRequest(request, openid_request): + """ + Store the openid request information in the session. + """ + if openid_request: + request.session['openid_request'] = openid_request + else: + request.session['openid_request'] = None + +def getRequest(request): + """ + Get an openid request from the session, if any. + """ + return request.session.get('openid_request') + +def server(request): + """ + Respond to requests for the server's primary web page. + """ + return direct_to_template( + request, + 'server/index.html', + {'user_url': getViewURL(request, idPage), + 'server_xrds_url': getViewURL(request, idpXrds), + }) + +def idpXrds(request): + """ + Respond to requests for the IDP's XRDS document, which is used in + IDP-driven identifier selection. + """ + return util.renderXRDS( + request, [OPENID_IDP_2_0_TYPE], [getViewURL(request, endpoint)]) + +def idPage(request): + """ + Serve the identity page for OpenID URLs. + """ + return direct_to_template( + request, + 'server/idPage.html', + {'server_url': getViewURL(request, endpoint)}) + +def trustPage(request): + """ + Display the trust page template, which allows the user to decide + whether to approve the OpenID verification. + """ + return direct_to_template( + request, + 'server/trust.html', + {'trust_handler_url':getViewURL(request, processTrustResult)}) + +def endpoint(request): + """ + Respond to low-level OpenID protocol messages. + """ + s = getServer(request) + + query = util.normalDict(request.GET or request.POST) + + # First, decode the incoming request into something the OpenID + # library can use. + try: + openid_request = s.decodeRequest(query) + except ProtocolError, why: + # This means the incoming request was invalid. + return direct_to_template( + request, + 'server/endpoint.html', + {'error': str(why)}) + + # If we did not get a request, display text indicating that this + # is an endpoint. + if openid_request is None: + return direct_to_template( + request, + 'server/endpoint.html', + {}) + + # We got a request; if the mode is checkid_*, we will handle it by + # getting feedback from the user or by checking the session. + if openid_request.mode in ["checkid_immediate", "checkid_setup"]: + return handleCheckIDRequest(request, openid_request) + else: + # We got some other kind of OpenID request, so we let the + # server handle this. + openid_response = s.handleRequest(openid_request) + return displayResponse(request, openid_response) + +def handleCheckIDRequest(request, openid_request): + """ + Handle checkid_* requests. Get input from the user to find out + whether she trusts the RP involved. Possibly, get intput about + what Simple Registration information, if any, to send in the + response. + """ + # If the request was an IDP-driven identifier selection request + # (i.e., the IDP URL was entered at the RP), then return the + # default identity URL for this server. In a full-featured + # provider, there could be interaction with the user to determine + # what URL should be sent. + if not openid_request.idSelect(): + + id_url = getViewURL(request, idPage) + + # Confirm that this server can actually vouch for that + # identifier + if id_url != openid_request.identity: + # Return an error response + error_response = ProtocolError( + openid_request.message, + "This server cannot verify the URL %r" % + (openid_request.identity,)) + + return displayResponse(request, error_response) + + if openid_request.immediate: + # Always respond with 'cancel' to immediate mode requests + # because we don't track information about a logged-in user. + # If we did, then the answer would depend on whether that user + # had trusted the request's trust root and whether the user is + # even logged in. + openid_response = openid_request.answer(False) + return displayResponse(request, openid_response) + else: + # Store the incoming request object in the session so we can + # get to it later. + setRequest(request, openid_request) + return showDecidePage(request, openid_request) + +def showDecidePage(request, openid_request): + """ + Render a page to the user so a trust decision can be made. + + @type openid_request: openid.server.server.CheckIDRequest + """ + trust_root = openid_request.trust_root + return_to = openid_request.return_to + + try: + # Stringify because template's ifequal can only compare to strings. + trust_root_valid = verifyReturnTo(trust_root, return_to) \ + and "Valid" or "Invalid" + except DiscoveryFailure, err: + trust_root_valid = "DISCOVERY_FAILED" + except HTTPFetchingError, err: + trust_root_valid = "Unreachable" + + pape_request = pape.Request.fromOpenIDRequest(openid_request) + + return direct_to_template( + request, + 'server/trust.html', + {'trust_root': trust_root, + 'trust_handler_url':getViewURL(request, processTrustResult), + 'trust_root_valid': trust_root_valid, + 'pape_request': pape_request, + }) + +def processTrustResult(request): + """ + Handle the result of a trust decision and respond to the RP + accordingly. + """ + # Get the request from the session so we can construct the + # appropriate response. + openid_request = getRequest(request) + + # The identifier that this server can vouch for + response_identity = getViewURL(request, idPage) + + # If the decision was to allow the verification, respond + # accordingly. + allowed = 'allow' in request.POST + + # Generate a response with the appropriate answer. + openid_response = openid_request.answer(allowed, + identity=response_identity) + + # Send Simple Registration data in the response, if appropriate. + if allowed: + sreg_data = { + 'fullname': 'Example User', + 'nickname': 'example', + 'dob': '1970-01-01', + 'email': 'invalid@example.com', + 'gender': 'F', + 'postcode': '12345', + 'country': 'ES', + 'language': 'eu', + 'timezone': 'America/New_York', + } + + sreg_req = sreg.SRegRequest.fromOpenIDRequest(openid_request) + sreg_resp = sreg.SRegResponse.extractResponse(sreg_req, sreg_data) + openid_response.addExtension(sreg_resp) + + pape_response = pape.Response() + pape_response.setAuthLevel(pape.LEVELS_NIST, 0) + openid_response.addExtension(pape_response) + + return displayResponse(request, openid_response) + +def displayResponse(request, openid_response): + """ + Display an OpenID response. Errors will be displayed directly to + the user; successful responses and other protocol-level messages + will be sent using the proper mechanism (i.e., direct response, + redirection, etc.). + """ + s = getServer(request) + + # Encode the response into something that is renderable. + try: + webresponse = s.encodeResponse(openid_response) + except EncodingError, why: + # If it couldn't be encoded, display an error. + text = why.response.encodeToKVForm() + return direct_to_template( + request, + 'server/endpoint.html', + {'error': cgi.escape(text)}) + + # Construct the appropriate django framework response. + r = http.HttpResponse(webresponse.body) + r.status_code = webresponse.code + + for header, value in webresponse.headers.iteritems(): + r[header] = value + + return r diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/settings.py b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/settings.py new file mode 100644 index 0000000..6d0fe0c --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/settings.py @@ -0,0 +1,84 @@ +# Django settings for djopenid project. + +import os +import sys +import warnings + +try: + import openid +except ImportError, e: + warnings.warn("Could not import OpenID library. Please consult the djopenid README.") + sys.exit(1) + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@domain.com'), +) + +MANAGERS = ADMINS + +DATABASE_ENGINE = 'sqlite3' # 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'. +DATABASE_NAME = '/tmp/test.db' # Or path to database file if using sqlite3. +DATABASE_USER = '' # Not used with sqlite3. +DATABASE_PASSWORD = '' # Not used with sqlite3. +DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. +DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. + +# Local time zone for this installation. All choices can be found here: +# http://www.postgresql.org/docs/current/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE +TIME_ZONE = 'America/Chicago' + +# Language code for this installation. All choices can be found here: +# http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes +# http://blogs.law.harvard.edu/tech/stories/storyReader$15 +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. +# Example: "http://media.lawrence.com" +MEDIA_URL = '' + +# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a +# trailing slash. +# Examples: "http://foo.com/media/", "/media/". +ADMIN_MEDIA_PREFIX = '/media/' + +# Make this unique, and don't share it with anybody. +SECRET_KEY = 'u^bw6lmsa6fah0$^lz-ct$)y7x7#ag92-z+y45-8!(jk0lkavy' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.load_template_source', + 'django.template.loaders.app_directories.load_template_source', +# 'django.template.loaders.eggs.load_template_source', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.middleware.doc.XViewMiddleware', +) + +ROOT_URLCONF = 'djopenid.urls' + +TEMPLATE_CONTEXT_PROCESSORS = () + +TEMPLATE_DIRS = ( + os.path.abspath(os.path.join(os.path.dirname(__file__), 'templates')), +) + +INSTALLED_APPS = ( + 'django.contrib.contenttypes', + 'django.contrib.sessions', + + 'djopenid.consumer', + 'djopenid.server', +) diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/consumer/index.html b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/consumer/index.html new file mode 100644 index 0000000..9b4926a --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/consumer/index.html @@ -0,0 +1,127 @@ + + + Django OpenID Example Consumer + + + + +
+ +

+ This is an example consumer built for the Django framework. Enter + an OpenID in the box below. +

+ + {% if error %} +
{{ error|escape }}
+ {% endif %} + + {% if url %} +
+ OpenID authentication succeeded; you authenticated as + {{ url|escape }}. + +

+ {% if sreg %} + Simple Registration data returned: + +

    + {% for pair in sreg %} +
  • {{ pair.0 }}: {{ pair.1 }}
  • + {% endfor %} +
+ {% else %} + The server returned no Simple Registration data. + {% endif %} + + {% if ax %} + Attribute Exchange data returned: + +
    + {% for pair in ax %} +
  • {{ pair.0 }}: {{ pair.1|join:", " }}
  • + {% endfor %} +
+ {% else %} + The server returned no Attribute Exchange data. + {% endif %} + + {% if pape %} + An authentication policy response contained these policies: + +
    + {% for uri in pape.auth_policies %} +
  • {{ uri }}
  • + {% endfor %} +
+ {% else %} + The server returned no authentication policy data (PAPE). + {% endif %} +

+
+ {% endif %} + + {% if message %} +
+ {{ message|escape }} +
+ {% endif %} + + {% if failure_reason %} +
+ {{ failure_reason|escape }} +
+ {% endif %} + +
+ + +

+ Request these authentication policies + (PAPE): + + + {% for pair in pape_policies %} + + + + + {% endfor %} +
+ +
+

+ + +
+ +
+ + + diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/consumer/request_form.html b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/consumer/request_form.html new file mode 100644 index 0000000..98018d6 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/consumer/request_form.html @@ -0,0 +1,5 @@ + + + {{ html }} + + diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/index.html b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/index.html new file mode 100644 index 0000000..62691ec --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/index.html @@ -0,0 +1,28 @@ + + + Python OpenID Example + + +

+ This is a Django package which implements both an OpenID server + and an OpenID consumer. These examples are provided with the + OpenID library so you can learn how to use it in your own + applications. +

+ +

+ To begin, click one of these links: +

+ + + +

+ Note: If you want to test the example consumer + using the example server, you must start a separate server process + for each application. +

+ + diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/endpoint.html b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/endpoint.html new file mode 100644 index 0000000..b2c9b41 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/endpoint.html @@ -0,0 +1,15 @@ + + + + This is an OpenID server endpoint. Your browser should never + actually request this page. + + {% if error %} + The OpenID server has encountered an error: +

+ {{ error|escape }} +

+ {% endif %} + + + diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/idPage.html b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/idPage.html new file mode 100644 index 0000000..06eb582 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/idPage.html @@ -0,0 +1,14 @@ +{% extends "server/index.html" %} + +{% block head %} + + + +{% endblock %} + +{% block body %} +

+This is the identity page for the OpenID that this server serves. +

+{% endblock %} diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/index.html b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/index.html new file mode 100644 index 0000000..01108d0 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/index.html @@ -0,0 +1,51 @@ + + + Django OpenID Example Server + + {% block head %} + + {% endblock %} + + + +{% block body %} +
+ +

+ This is an example server built for the Django framework. It only + authenticates one OpenID, which is also served by this + application. The OpenID it serves is + +

+{{ user_url }}
+    
+

+ +
+{% endblock %} + + diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/pape_request_info.html b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/pape_request_info.html new file mode 100644 index 0000000..3a0179f --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/pape_request_info.html @@ -0,0 +1,21 @@ +{% if pape_request %} + {% if pape_request.preferred_auth_policies %} + The relying party requested the following PAPE policies be in effect: + +
    + {% for uri in pape_request.preferred_auth_policies %} +
  • {{ uri }}
  • + {% endfor %} +
+ {% endif %} + + {% if pape_request.preferred_auth_level_types %} + The relying party requested the following authentication level types: + +
    + {% for uri in pape_request.preferred_auth_level_types %} +
  • {{ uri }}
  • + {% endfor %} +
+ {% endif %} +{% endif %} diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/trust.html b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/trust.html new file mode 100644 index 0000000..815ab85 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/server/trust.html @@ -0,0 +1,51 @@ +{% extends "server/index.html" %} + +{% block body %} + +{% ifequal trust_root_valid "Valid" %} + +

The site {{ trust_root|escape }} has requested verification + of your OpenID.

+ + {% include "server/pape_request_info.html" %} +{% endifequal %} +{% ifequal trust_root_valid "Invalid" %} +
+

This request claims to be from {{ trust_root|escape }} but I have + determined that it is a pack of lies. Beware, if you release + information to them, they are likely to do unconscionable things with it, + being the lying liars that they are.

+

Please tell the real {{ trust_root|escape }} that someone is + trying to abuse your trust in their good name.

+
+{% endifequal %} +{% ifequal trust_root_valid "Unreachable" %} +

The site {{ trust_root|escape }} has requested verification + of your OpenID. I have failed to reach it and thus cannot vouch for its + authenticity. Perhaps it is on your local network.

+{% endifequal %} +{% ifequal trust_root_valid "DISCOVERY_FAILED" %} +

The site {{ trust_root|escape }} has requested verification + of your OpenID. However, {{ trust_root|escape }} does not + implement OpenID 2.0's relying party verification mechanism. Please use + extra caution in deciding whether to release information to this party, + and ask {{ trust_root|escape }} to implement relying party + verification for your future transactions.

+ + {% include "server/pape_request_info.html" %} +{% endifequal %} + + + +
+
+ Verify your identity to the relying party? + +
+ + +
+
+ +{% endblock %} diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/xrds.xml b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/xrds.xml new file mode 100644 index 0000000..fc684e6 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/templates/xrds.xml @@ -0,0 +1,17 @@ + + + + + + {% for type_uri in type_uris %} + {{ type_uri|escape }} + {% endfor %} + {% for endpoint_url in endpoint_urls %} + {{ endpoint_url }} + {% endfor %} + + + + diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/urls.py b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/urls.py new file mode 100644 index 0000000..d91ee1f --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns( + '', + ('^$', 'djopenid.views.index'), + ('^consumer/', include('djopenid.consumer.urls')), + ('^server/', include('djopenid.server.urls')), +) diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/util.py b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/util.py new file mode 100644 index 0000000..2da8583 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/util.py @@ -0,0 +1,147 @@ + +""" +Utility code for the Django example consumer and server. +""" + +from urlparse import urljoin + +from django.db import connection +from django.template.context import RequestContext +from django.template import loader +from django import http +from django.core.exceptions import ImproperlyConfigured +from django.core.urlresolvers import reverse as reverseURL +from django.views.generic.simple import direct_to_template + +from django.conf import settings + +from openid.store.filestore import FileOpenIDStore +from openid.store import sqlstore +from openid.yadis.constants import YADIS_CONTENT_TYPE + +def getOpenIDStore(filestore_path, table_prefix): + """ + Returns an OpenID association store object based on the database + engine chosen for this Django application. + + * If no database engine is chosen, a filesystem-based store will + be used whose path is filestore_path. + + * If a database engine is chosen, a store object for that database + type will be returned. + + * If the chosen engine is not supported by the OpenID library, + raise ImproperlyConfigured. + + * If a database store is used, this will create the tables + necessary to use it. The table names will be prefixed with + table_prefix. DO NOT use the same table prefix for both an + OpenID consumer and an OpenID server in the same database. + + The result of this function should be passed to the Consumer + constructor as the store parameter. + """ + if not settings.DATABASE_ENGINE: + return FileOpenIDStore(filestore_path) + + # Possible side-effect: create a database connection if one isn't + # already open. + connection.cursor() + + # Create table names to specify for SQL-backed stores. + tablenames = { + 'associations_table': table_prefix + 'openid_associations', + 'nonces_table': table_prefix + 'openid_nonces', + } + + types = { + 'postgresql': sqlstore.PostgreSQLStore, + 'mysql': sqlstore.MySQLStore, + 'sqlite3': sqlstore.SQLiteStore, + } + + try: + s = types[settings.DATABASE_ENGINE](connection.connection, + **tablenames) + except KeyError: + raise ImproperlyConfigured, \ + "Database engine %s not supported by OpenID library" % \ + (settings.DATABASE_ENGINE,) + + try: + s.createTables() + except (SystemExit, KeyboardInterrupt, MemoryError), e: + raise + except: + # XXX This is not the Right Way to do this, but because the + # underlying database implementation might differ in behavior + # at this point, we can't reliably catch the right + # exception(s) here. Ideally, the SQL store in the OpenID + # library would catch exceptions that it expects and fail + # silently, but that could be bad, too. More ideally, the SQL + # store would not attempt to create tables it knows already + # exists. + pass + + return s + +def getViewURL(req, view_name_or_obj, args=None, kwargs=None): + relative_url = reverseURL(view_name_or_obj, args=args, kwargs=kwargs) + full_path = req.META.get('SCRIPT_NAME', '') + relative_url + return urljoin(getBaseURL(req), full_path) + +def getBaseURL(req): + """ + Given a Django web request object, returns the OpenID 'trust root' + for that request; namely, the absolute URL to the site root which + is serving the Django request. The trust root will include the + proper scheme and authority. It will lack a port if the port is + standard (80, 443). + """ + name = req.META['HTTP_HOST'] + try: + name = name[:name.index(':')] + except: + pass + + try: + port = int(req.META['SERVER_PORT']) + except: + port = 80 + + proto = req.META['SERVER_PROTOCOL'] + + if 'HTTPS' in proto: + proto = 'https' + else: + proto = 'http' + + if port in [80, 443] or not port: + port = '' + else: + port = ':%s' % (port,) + + url = "%s://%s%s/" % (proto, name, port) + return url + +def normalDict(request_data): + """ + Converts a django request MutliValueDict (e.g., request.GET, + request.POST) into a standard python dict whose values are the + first value from each of the MultiValueDict's value lists. This + avoids the OpenID library's refusal to deal with dicts whose + values are lists, because in OpenID, each key in the query arg set + can have at most one value. + """ + return dict((k, v[0]) for k, v in request_data.iteritems()) + +def renderXRDS(request, type_uris, endpoint_urls): + """Render an XRDS page with the specified type URIs and endpoint + URLs in one service block, and return a response with the + appropriate content-type. + """ + response = direct_to_template( + request, 'xrds.xml', + {'type_uris':type_uris, 'endpoint_urls':endpoint_urls,}) + response['Content-Type'] = YADIS_CONTENT_TYPE + return response diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/views.py b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/views.py new file mode 100644 index 0000000..5d399d6 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/djopenid/views.py @@ -0,0 +1,14 @@ + +from djopenid import util +from django.views.generic.simple import direct_to_template + +def index(request): + consumer_url = util.getViewURL( + request, 'djopenid.consumer.views.startOpenID') + server_url = util.getViewURL(request, 'djopenid.server.views.server') + + return direct_to_template( + request, + 'index.html', + {'consumer_url':consumer_url, 'server_url':server_url}) + diff --git a/desktop/core/ext-py/python-openid-2.2.5/examples/server.py b/desktop/core/ext-py/python-openid-2.2.5/examples/server.py new file mode 100644 index 0000000..3adc61b --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/examples/server.py @@ -0,0 +1,721 @@ +#!/usr/bin/env python + +__copyright__ = 'Copyright 2005-2008, Janrain, Inc.' + +from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler +from urlparse import urlparse + +import time +import Cookie +import cgi +import cgitb +import sys + +def quoteattr(s): + qs = cgi.escape(s, 1) + return '"%s"' % (qs,) + +try: + import openid +except ImportError: + sys.stderr.write(""" +Failed to import the OpenID library. In order to use this example, you +must either install the library (see INSTALL in the root of the +distribution) or else add the library to python's import path (the +PYTHONPATH environment variable). + +For more information, see the README in the root of the library +distribution.""") + sys.exit(1) + +from openid.extensions import sreg +from openid.server import server +from openid.store.filestore import FileOpenIDStore +from openid.consumer import discover + +class OpenIDHTTPServer(HTTPServer): + """ + http server that contains a reference to an OpenID Server and + knows its base URL. + """ + def __init__(self, *args, **kwargs): + HTTPServer.__init__(self, *args, **kwargs) + + if self.server_port != 80: + self.base_url = ('http://%s:%s/' % + (self.server_name, self.server_port)) + else: + self.base_url = 'http://%s/' % (self.server_name,) + + self.openid = None + self.approved = {} + self.lastCheckIDRequest = {} + + def setOpenIDServer(self, oidserver): + self.openid = oidserver + + +class ServerHandler(BaseHTTPRequestHandler): + def __init__(self, *args, **kwargs): + self.user = None + BaseHTTPRequestHandler.__init__(self, *args, **kwargs) + + + def do_GET(self): + try: + self.parsed_uri = urlparse(self.path) + self.query = {} + for k, v in cgi.parse_qsl(self.parsed_uri[4]): + self.query[k] = v + + self.setUser() + + path = self.parsed_uri[2].lower() + + if path == '/': + self.showMainPage() + elif path == '/openidserver': + self.serverEndPoint(self.query) + + elif path == '/login': + self.showLoginPage('/', '/') + elif path == '/loginsubmit': + self.doLogin() + elif path.startswith('/id/'): + self.showIdPage(path) + elif path.startswith('/yadis/'): + self.showYadis(path[7:]) + elif path == '/serveryadis': + self.showServerYadis() + else: + self.send_response(404) + self.end_headers() + + except (KeyboardInterrupt, SystemExit): + raise + except: + self.send_response(500) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(cgitb.html(sys.exc_info(), context=10)) + + def do_POST(self): + try: + self.parsed_uri = urlparse(self.path) + + self.setUser() + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length) + + self.query = {} + for k, v in cgi.parse_qsl(post_data): + self.query[k] = v + + path = self.parsed_uri[2] + if path == '/openidserver': + self.serverEndPoint(self.query) + + elif path == '/allow': + self.handleAllow(self.query) + else: + self.send_response(404) + self.end_headers() + + except (KeyboardInterrupt, SystemExit): + raise + except: + self.send_response(500) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(cgitb.html(sys.exc_info(), context=10)) + + def handleAllow(self, query): + # pretend this next bit is keying off the user's session or something, + # right? + request = self.server.lastCheckIDRequest.get(self.user) + + if 'yes' in query: + if 'login_as' in query: + self.user = self.query['login_as'] + + if request.idSelect(): + identity = self.server.base_url + 'id/' + query['identifier'] + else: + identity = request.identity + + trust_root = request.trust_root + if self.query.get('remember', 'no') == 'yes': + self.server.approved[(identity, trust_root)] = 'always' + + response = self.approved(request, identity) + + elif 'no' in query: + response = request.answer(False) + + else: + assert False, 'strange allow post. %r' % (query,) + + self.displayResponse(response) + + + def setUser(self): + cookies = self.headers.get('Cookie') + if cookies: + morsel = Cookie.BaseCookie(cookies).get('user') + if morsel: + self.user = morsel.value + + def isAuthorized(self, identity_url, trust_root): + if self.user is None: + return False + + if identity_url != self.server.base_url + 'id/' + self.user: + return False + + key = (identity_url, trust_root) + return self.server.approved.get(key) is not None + + def serverEndPoint(self, query): + try: + request = self.server.openid.decodeRequest(query) + except server.ProtocolError, why: + self.displayResponse(why) + return + + if request is None: + # Display text indicating that this is an endpoint. + self.showAboutPage() + return + + if request.mode in ["checkid_immediate", "checkid_setup"]: + self.handleCheckIDRequest(request) + else: + response = self.server.openid.handleRequest(request) + self.displayResponse(response) + + def addSRegResponse(self, request, response): + sreg_req = sreg.SRegRequest.fromOpenIDRequest(request) + + # In a real application, this data would be user-specific, + # and the user should be asked for permission to release + # it. + sreg_data = { + 'nickname':self.user + } + + sreg_resp = sreg.SRegResponse.extractResponse(sreg_req, sreg_data) + response.addExtension(sreg_resp) + + def approved(self, request, identifier=None): + response = request.answer(True, identity=identifier) + self.addSRegResponse(request, response) + return response + + def handleCheckIDRequest(self, request): + is_authorized = self.isAuthorized(request.identity, request.trust_root) + if is_authorized: + response = self.approved(request) + self.displayResponse(response) + elif request.immediate: + response = request.answer(False) + self.displayResponse(response) + else: + self.server.lastCheckIDRequest[self.user] = request + self.showDecidePage(request) + + def displayResponse(self, response): + try: + webresponse = self.server.openid.encodeResponse(response) + except server.EncodingError, why: + text = why.response.encodeToKVForm() + self.showErrorPage('
%s
' % cgi.escape(text)) + return + + self.send_response(webresponse.code) + for header, value in webresponse.headers.iteritems(): + self.send_header(header, value) + self.writeUserHeader() + self.end_headers() + + if webresponse.body: + self.wfile.write(webresponse.body) + + def doLogin(self): + if 'submit' in self.query: + if 'user' in self.query: + self.user = self.query['user'] + else: + self.user = None + self.redirect(self.query['success_to']) + elif 'cancel' in self.query: + self.redirect(self.query['fail_to']) + else: + assert 0, 'strange login %r' % (self.query,) + + def redirect(self, url): + self.send_response(302) + self.send_header('Location', url) + self.writeUserHeader() + + self.end_headers() + + def writeUserHeader(self): + if self.user is None: + t1970 = time.gmtime(0) + expires = time.strftime( + 'Expires=%a, %d-%b-%y %H:%M:%S GMT', t1970) + self.send_header('Set-Cookie', 'user=;%s' % expires) + else: + self.send_header('Set-Cookie', 'user=%s' % self.user) + + def showAboutPage(self): + endpoint_url = self.server.base_url + 'openidserver' + + def link(url): + url_attr = quoteattr(url) + url_text = cgi.escape(url) + return '%s' % (url_attr, url_text) + + def term(url, text): + return '
%s
%s
' % (link(url), text) + + resources = [ + (self.server.base_url, "This example server's home page"), + ('http://www.openidenabled.com/', + 'An OpenID community Web site, home of this library'), + ('http://www.openid.net/', 'the official OpenID Web site'), + ] + + resource_markup = ''.join([term(url, text) for url, text in resources]) + + self.showPage(200, 'This is an OpenID server', msg="""\ +

%s is an OpenID server endpoint.

+

For more information about OpenID, see:

+
+ %s +
+ """ % (link(endpoint_url), resource_markup,)) + + def showErrorPage(self, error_message): + self.showPage(400, 'Error Processing Request', err='''\ +

%s

+ + ''' % error_message) + + def showDecidePage(self, request): + id_url_base = self.server.base_url+'id/' + # XXX: This may break if there are any synonyms for id_url_base, + # such as referring to it by IP address or a CNAME. + assert (request.identity.startswith(id_url_base) or + request.idSelect()), repr((request.identity, id_url_base)) + expected_user = request.identity[len(id_url_base):] + + if request.idSelect(): # We are being asked to select an ID + msg = '''\ +

A site has asked for your identity. You may select an + identifier by which you would like this site to know you. + On a production site this would likely be a drop down list + of pre-created accounts or have the facility to generate + a random anonymous identifier. +

+ ''' + fdata = { + 'id_url_base': id_url_base, + 'trust_root': request.trust_root, + } + form = '''\ +
+ + + + +
Identity:%(id_url_base)s
Trust Root:%(trust_root)s
+

Allow this authentication to proceed?

+
+ + +
+ '''%fdata + elif expected_user == self.user: + msg = '''\ +

A new site has asked to confirm your identity. If you + approve, the site represented by the trust root below will + be told that you control identity URL listed below. (If + you are using a delegated identity, the site will take + care of reversing the delegation on its own.)

''' + + fdata = { + 'identity': request.identity, + 'trust_root': request.trust_root, + } + form = '''\ + + + +
Identity:%(identity)s
Trust Root:%(trust_root)s
+

Allow this authentication to proceed?

+
+
+ + +
''' % fdata + else: + mdata = { + 'expected_user': expected_user, + 'user': self.user, + } + msg = '''\ +

A site has asked for an identity belonging to + %(expected_user)s, but you are logged in as %(user)s. To + log in as %(expected_user)s and approve the login request, + hit OK below. The "Remember this decision" checkbox + applies only to the trust root decision.

''' % mdata + + fdata = { + 'identity': request.identity, + 'trust_root': request.trust_root, + 'expected_user': expected_user, + } + form = '''\ + + + +
Identity:%(identity)s
Trust Root:%(trust_root)s
+

Allow this authentication to proceed?

+
+
+ + + +
''' % fdata + + self.showPage(200, 'Approve OpenID request?', msg=msg, form=form) + + def showIdPage(self, path): + link_tag = '' %\ + self.server.base_url + yadis_loc_tag = ''%\ + (self.server.base_url+'yadis/'+path[4:]) + disco_tags = link_tag + yadis_loc_tag + ident = self.server.base_url + path[1:] + + approved_trust_roots = [] + for (aident, trust_root) in self.server.approved.keys(): + if aident == ident: + trs = '
  • %s
  • \n' % cgi.escape(trust_root) + approved_trust_roots.append(trs) + + if approved_trust_roots: + prepend = '

    Approved trust roots:

    \n
      \n' + approved_trust_roots.insert(0, prepend) + approved_trust_roots.append('
    \n') + msg = ''.join(approved_trust_roots) + else: + msg = '' + + self.showPage(200, 'An Identity Page', head_extras=disco_tags, msg='''\ +

    This is an identity page for %s.

    + %s + ''' % (ident, msg)) + + def showYadis(self, user): + self.send_response(200) + self.send_header('Content-type', 'application/xrds+xml') + self.end_headers() + + endpoint_url = self.server.base_url + 'openidserver' + user_url = self.server.base_url + 'id/' + user + self.wfile.write("""\ + + + + + + %s + %s + %s + %s + + + + +"""%(discover.OPENID_2_0_TYPE, discover.OPENID_1_0_TYPE, + endpoint_url, user_url)) + + def showServerYadis(self): + self.send_response(200) + self.send_header('Content-type', 'application/xrds+xml') + self.end_headers() + + endpoint_url = self.server.base_url + 'openidserver' + self.wfile.write("""\ + + + + + + %s + %s + + + + +"""%(discover.OPENID_IDP_2_0_TYPE, endpoint_url,)) + + def showMainPage(self): + yadis_tag = ''%\ + (self.server.base_url + 'serveryadis') + if self.user: + openid_url = self.server.base_url + 'id/' + self.user + user_message = """\ +

    You are logged in as %s. Your OpenID identity URL is + %s. Enter that URL at an OpenID + consumer to test this server.

    + """ % (self.user, quoteattr(openid_url), openid_url) + else: + user_message = """\ +

    This server uses a cookie to remember who you are in + order to simulate a standard Web user experience. You are + not logged in.

    """ + + self.showPage(200, 'Main Page', head_extras = yadis_tag, msg='''\ +

    This is a simple OpenID server implemented using the Python OpenID + library.

    + + %s + +

    To use this server with a consumer, the consumer must be + able to fetch HTTP pages from this web server. If this + computer is behind a firewall, you will not be able to use + OpenID consumers outside of the firewall with it.

    + +

    The URL for this server is %s.

    + ''' % (user_message, quoteattr(self.server.base_url), self.server.base_url)) + + def showLoginPage(self, success_to, fail_to): + self.showPage(200, 'Login Page', form='''\ +

    Login

    +

    You may log in with any name. This server does not use + passwords because it is just a sample of how to use the OpenID + library.

    +
    + + + + + +
    + ''' % (success_to, fail_to)) + + def showPage(self, response_code, title, + head_extras='', msg=None, err=None, form=None): + + if self.user is None: + user_link = 'not logged in.' + else: + user_link = 'logged in as %s.
    Log out' % \ + (self.user, self.user) + + body = '' + + if err is not None: + body += '''\ +
    + %s +
    + ''' % err + + if msg is not None: + body += '''\ +
    + %s +
    + ''' % msg + + if form is not None: + body += '''\ +
    + %s +
    + ''' % form + + contents = { + 'title': 'Python OpenID Server Example - ' + title, + 'head_extras': head_extras, + 'body': body, + 'user_link': user_link, + } + + self.send_response(response_code) + self.writeUserHeader() + self.send_header('Content-type', 'text/html') + self.end_headers() + + self.wfile.write(''' + + %(title)s + %(head_extras)s + + + + + + + + + +%(body)s + + +''' % contents) + + +def main(host, port, data_path): + addr = (host, port) + httpserver = OpenIDHTTPServer(addr, ServerHandler) + + # Instantiate OpenID consumer store and OpenID consumer. If you + # were connecting to a database, you would create the database + # connection and instantiate an appropriate store here. + store = FileOpenIDStore(data_path) + oidserver = server.Server(store, httpserver.base_url + 'openidserver') + + httpserver.setOpenIDServer(oidserver) + + print 'Server running at:' + print httpserver.base_url + httpserver.serve_forever() + +if __name__ == '__main__': + host = 'localhost' + data_path = 'sstore' + port = 8000 + + try: + import optparse + except ImportError: + pass # Use defaults (for Python 2.2) + else: + parser = optparse.OptionParser('Usage:\n %prog [options]') + parser.add_option( + '-d', '--data-path', dest='data_path', default=data_path, + help='Data directory for storing OpenID consumer state. ' + 'Defaults to "%default" in the current directory.') + parser.add_option( + '-p', '--port', dest='port', type='int', default=port, + help='Port on which to listen for HTTP requests. ' + 'Defaults to port %default.') + parser.add_option( + '-s', '--host', dest='host', default=host, + help='Host on which to listen for HTTP requests. ' + 'Also used for generating URLs. Defaults to %default.') + + options, args = parser.parse_args() + if args: + parser.error('Expected no arguments. Got %r' % args) + + host = options.host + port = options.port + data_path = options.data_path + + main(host, port, data_path) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/__init__.py b/desktop/core/ext-py/python-openid-2.2.5/openid/__init__.py new file mode 100644 index 0000000..6be6538 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/__init__.py @@ -0,0 +1,55 @@ +""" +This package is an implementation of the OpenID specification in +Python. It contains code for both server and consumer +implementations. For information on implementing an OpenID consumer, +see the C{L{openid.consumer.consumer}} module. For information on +implementing an OpenID server, see the C{L{openid.server.server}} +module. + +@contact: U{http://openid.net/developers/dev-mailing-lists/ + }, +L{server} and +L{consumer} objects will create and manage +the associations. The consumer and server code will make use of a +C{L{SessionNegotiator}} when managing associations, which enables +users to express a preference for what kind of associations should be +allowed, and what kind of exchange should be done to establish the +association. + +@var default_negotiator: A C{L{SessionNegotiator}} that allows all + association types that are specified by the OpenID + specification. It prefers to use HMAC-SHA1/DH-SHA1, if it's + available. If HMAC-SHA256 is not supported by your Python runtime, + HMAC-SHA256 and DH-SHA256 will not be available. + +@var encrypted_negotiator: A C{L{SessionNegotiator}} that + does not support C{'no-encryption'} associations. It prefers + HMAC-SHA1/DH-SHA1 association types if available. +""" + +__all__ = [ + 'default_negotiator', + 'encrypted_negotiator', + 'SessionNegotiator', + 'Association', + ] + +import time + +from openid import cryptutil +from openid import kvform +from openid import oidutil +from openid.message import OPENID_NS + +all_association_types = [ + 'HMAC-SHA1', + 'HMAC-SHA256', + ] + +if hasattr(cryptutil, 'hmacSha256'): + supported_association_types = list(all_association_types) + + default_association_order = [ + ('HMAC-SHA1', 'DH-SHA1'), + ('HMAC-SHA1', 'no-encryption'), + ('HMAC-SHA256', 'DH-SHA256'), + ('HMAC-SHA256', 'no-encryption'), + ] + + only_encrypted_association_order = [ + ('HMAC-SHA1', 'DH-SHA1'), + ('HMAC-SHA256', 'DH-SHA256'), + ] +else: + supported_association_types = ['HMAC-SHA1'] + + default_association_order = [ + ('HMAC-SHA1', 'DH-SHA1'), + ('HMAC-SHA1', 'no-encryption'), + ] + + only_encrypted_association_order = [ + ('HMAC-SHA1', 'DH-SHA1'), + ] + +def getSessionTypes(assoc_type): + """Return the allowed session types for a given association type""" + assoc_to_session = { + 'HMAC-SHA1': ['DH-SHA1', 'no-encryption'], + 'HMAC-SHA256': ['DH-SHA256', 'no-encryption'], + } + return assoc_to_session.get(assoc_type, []) + +def checkSessionType(assoc_type, session_type): + """Check to make sure that this pair of assoc type and session + type are allowed""" + if session_type not in getSessionTypes(assoc_type): + raise ValueError( + 'Session type %r not valid for assocation type %r' + % (session_type, assoc_type)) + +class SessionNegotiator(object): + """A session negotiator controls the allowed and preferred + association types and association session types. Both the + C{L{Consumer}} and + C{L{Server}} use negotiators when + creating associations. + + You can create and use negotiators if you: + + - Do not want to do Diffie-Hellman key exchange because you use + transport-layer encryption (e.g. SSL) + + - Want to use only SHA-256 associations + + - Do not want to support plain-text associations over a non-secure + channel + + It is up to you to set a policy for what kinds of associations to + accept. By default, the library will make any kind of association + that is allowed in the OpenID 2.0 specification. + + Use of negotiators in the library + ================================= + + When a consumer makes an association request, it calls + C{L{getAllowedType}} to get the preferred association type and + association session type. + + The server gets a request for a particular association/session + type and calls C{L{isAllowed}} to determine if it should + create an association. If it is supported, negotiation is + complete. If it is not, the server calls C{L{getAllowedType}} to + get an allowed association type to return to the consumer. + + If the consumer gets an error response indicating that the + requested association/session type is not supported by the server + that contains an assocation/session type to try, it calls + C{L{isAllowed}} to determine if it should try again with the + given combination of association/session type. + + @ivar allowed_types: A list of association/session types that are + allowed by the server. The order of the pairs in this list + determines preference. If an association/session type comes + earlier in the list, the library is more likely to use that + type. + @type allowed_types: [(str, str)] + """ + + def __init__(self, allowed_types): + self.setAllowedTypes(allowed_types) + + def copy(self): + return self.__class__(list(self.allowed_types)) + + def setAllowedTypes(self, allowed_types): + """Set the allowed association types, checking to make sure + each combination is valid.""" + for (assoc_type, session_type) in allowed_types: + checkSessionType(assoc_type, session_type) + + self.allowed_types = allowed_types + + def addAllowedType(self, assoc_type, session_type=None): + """Add an association type and session type to the allowed + types list. The assocation/session pairs are tried in the + order that they are added.""" + if self.allowed_types is None: + self.allowed_types = [] + + if session_type is None: + available = getSessionTypes(assoc_type) + + if not available: + raise ValueError('No session available for association type %r' + % (assoc_type,)) + + for session_type in getSessionTypes(assoc_type): + self.addAllowedType(assoc_type, session_type) + else: + checkSessionType(assoc_type, session_type) + self.allowed_types.append((assoc_type, session_type)) + + + def isAllowed(self, assoc_type, session_type): + """Is this combination of association type and session type allowed?""" + assoc_good = (assoc_type, session_type) in self.allowed_types + matches = session_type in getSessionTypes(assoc_type) + return assoc_good and matches + + def getAllowedType(self): + """Get a pair of assocation type and session type that are + supported""" + try: + return self.allowed_types[0] + except IndexError: + return (None, None) + +default_negotiator = SessionNegotiator(default_association_order) +encrypted_negotiator = SessionNegotiator(only_encrypted_association_order) + +def getSecretSize(assoc_type): + if assoc_type == 'HMAC-SHA1': + return 20 + elif assoc_type == 'HMAC-SHA256': + return 32 + else: + raise ValueError('Unsupported association type: %r' % (assoc_type,)) + +class Association(object): + """ + This class represents an association between a server and a + consumer. In general, users of this library will never see + instances of this object. The only exception is if you implement + a custom C{L{OpenIDStore}}. + + If you do implement such a store, it will need to store the values + of the C{L{handle}}, C{L{secret}}, C{L{issued}}, C{L{lifetime}}, and + C{L{assoc_type}} instance variables. + + @ivar handle: This is the handle the server gave this association. + + @type handle: C{str} + + + @ivar secret: This is the shared secret the server generated for + this association. + + @type secret: C{str} + + + @ivar issued: This is the time this association was issued, in + seconds since 00:00 GMT, January 1, 1970. (ie, a unix + timestamp) + + @type issued: C{int} + + + @ivar lifetime: This is the amount of time this association is + good for, measured in seconds since the association was + issued. + + @type lifetime: C{int} + + + @ivar assoc_type: This is the type of association this instance + represents. The only valid value of this field at this time + is C{'HMAC-SHA1'}, but new types may be defined in the future. + + @type assoc_type: C{str} + + + @sort: __init__, fromExpiresIn, getExpiresIn, __eq__, __ne__, + handle, secret, issued, lifetime, assoc_type + """ + + # The ordering and name of keys as stored by serialize + assoc_keys = [ + 'version', + 'handle', + 'secret', + 'issued', + 'lifetime', + 'assoc_type', + ] + + + _macs = { + 'HMAC-SHA1': cryptutil.hmacSha1, + 'HMAC-SHA256': cryptutil.hmacSha256, + } + + + def fromExpiresIn(cls, expires_in, handle, secret, assoc_type): + """ + This is an alternate constructor used by the OpenID consumer + library to create associations. C{L{OpenIDStore + }} implementations + shouldn't use this constructor. + + + @param expires_in: This is the amount of time this association + is good for, measured in seconds since the association was + issued. + + @type expires_in: C{int} + + + @param handle: This is the handle the server gave this + association. + + @type handle: C{str} + + + @param secret: This is the shared secret the server generated + for this association. + + @type secret: C{str} + + + @param assoc_type: This is the type of association this + instance represents. The only valid value of this field + at this time is C{'HMAC-SHA1'}, but new types may be + defined in the future. + + @type assoc_type: C{str} + """ + issued = int(time.time()) + lifetime = expires_in + return cls(handle, secret, issued, lifetime, assoc_type) + + fromExpiresIn = classmethod(fromExpiresIn) + + def __init__(self, handle, secret, issued, lifetime, assoc_type): + """ + This is the standard constructor for creating an association. + + + @param handle: This is the handle the server gave this + association. + + @type handle: C{str} + + + @param secret: This is the shared secret the server generated + for this association. + + @type secret: C{str} + + + @param issued: This is the time this association was issued, + in seconds since 00:00 GMT, January 1, 1970. (ie, a unix + timestamp) + + @type issued: C{int} + + + @param lifetime: This is the amount of time this association + is good for, measured in seconds since the association was + issued. + + @type lifetime: C{int} + + + @param assoc_type: This is the type of association this + instance represents. The only valid value of this field + at this time is C{'HMAC-SHA1'}, but new types may be + defined in the future. + + @type assoc_type: C{str} + """ + if assoc_type not in all_association_types: + fmt = '%r is not a supported association type' + raise ValueError(fmt % (assoc_type,)) + +# secret_size = getSecretSize(assoc_type) +# if len(secret) != secret_size: +# fmt = 'Wrong size secret (%s bytes) for association type %s' +# raise ValueError(fmt % (len(secret), assoc_type)) + + self.handle = handle + self.secret = secret + self.issued = issued + self.lifetime = lifetime + self.assoc_type = assoc_type + + def getExpiresIn(self, now=None): + """ + This returns the number of seconds this association is still + valid for, or C{0} if the association is no longer valid. + + + @return: The number of seconds this association is still valid + for, or C{0} if the association is no longer valid. + + @rtype: C{int} + """ + if now is None: + now = int(time.time()) + + return max(0, self.issued + self.lifetime - now) + + expiresIn = property(getExpiresIn) + + def __eq__(self, other): + """ + This checks to see if two C{L{Association}} instances + represent the same association. + + + @return: C{True} if the two instances represent the same + association, C{False} otherwise. + + @rtype: C{bool} + """ + return type(self) is type(other) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + """ + This checks to see if two C{L{Association}} instances + represent different associations. + + + @return: C{True} if the two instances represent different + associations, C{False} otherwise. + + @rtype: C{bool} + """ + return not (self == other) + + def serialize(self): + """ + Convert an association to KV form. + + @return: String in KV form suitable for deserialization by + deserialize. + + @rtype: str + """ + data = { + 'version':'2', + 'handle':self.handle, + 'secret':oidutil.toBase64(self.secret), + 'issued':str(int(self.issued)), + 'lifetime':str(int(self.lifetime)), + 'assoc_type':self.assoc_type + } + + assert len(data) == len(self.assoc_keys) + pairs = [] + for field_name in self.assoc_keys: + pairs.append((field_name, data[field_name])) + + return kvform.seqToKV(pairs, strict=True) + + def deserialize(cls, assoc_s): + """ + Parse an association as stored by serialize(). + + inverse of serialize + + + @param assoc_s: Association as serialized by serialize() + + @type assoc_s: str + + + @return: instance of this class + """ + pairs = kvform.kvToSeq(assoc_s, strict=True) + keys = [] + values = [] + for k, v in pairs: + keys.append(k) + values.append(v) + + if keys != cls.assoc_keys: + raise ValueError('Unexpected key values: %r', keys) + + version, handle, secret, issued, lifetime, assoc_type = values + if version != '2': + raise ValueError('Unknown version: %r' % version) + issued = int(issued) + lifetime = int(lifetime) + secret = oidutil.fromBase64(secret) + return cls(handle, secret, issued, lifetime, assoc_type) + + deserialize = classmethod(deserialize) + + def sign(self, pairs): + """ + Generate a signature for a sequence of (key, value) pairs + + + @param pairs: The pairs to sign, in order + + @type pairs: sequence of (str, str) + + + @return: The binary signature of this sequence of pairs + + @rtype: str + """ + kv = kvform.seqToKV(pairs) + + try: + mac = self._macs[self.assoc_type] + except KeyError: + raise ValueError( + 'Unknown association type: %r' % (self.assoc_type,)) + + return mac(self.secret, kv) + + + def getMessageSignature(self, message): + """Return the signature of a message. + + If I am not a sign-all association, the message must have a + signed list. + + @return: the signature, base64 encoded + + @rtype: str + + @raises ValueError: If there is no signed list and I am not a sign-all + type of association. + """ + pairs = self._makePairs(message) + return oidutil.toBase64(self.sign(pairs)) + + def signMessage(self, message): + """Add a signature (and a signed list) to a message. + + @return: a new Message object with a signature + @rtype: L{openid.message.Message} + """ + if (message.hasKey(OPENID_NS, 'sig') or + message.hasKey(OPENID_NS, 'signed')): + raise ValueError('Message already has signed list or signature') + + extant_handle = message.getArg(OPENID_NS, 'assoc_handle') + if extant_handle and extant_handle != self.handle: + raise ValueError("Message has a different association handle") + + signed_message = message.copy() + signed_message.setArg(OPENID_NS, 'assoc_handle', self.handle) + message_keys = signed_message.toPostArgs().keys() + signed_list = [k[7:] for k in message_keys + if k.startswith('openid.')] + signed_list.append('signed') + signed_list.sort() + signed_message.setArg(OPENID_NS, 'signed', ','.join(signed_list)) + sig = self.getMessageSignature(signed_message) + signed_message.setArg(OPENID_NS, 'sig', sig) + return signed_message + + def checkMessageSignature(self, message): + """Given a message with a signature, calculate a new signature + and return whether it matches the signature in the message. + + @raises ValueError: if the message has no signature or no signature + can be calculated for it. + """ + message_sig = message.getArg(OPENID_NS, 'sig') + if not message_sig: + raise ValueError("%s has no sig." % (message,)) + calculated_sig = self.getMessageSignature(message) + return calculated_sig == message_sig + + + def _makePairs(self, message): + signed = message.getArg(OPENID_NS, 'signed') + if not signed: + raise ValueError('Message has no signed list: %s' % (message,)) + + signed_list = signed.split(',') + pairs = [] + data = message.toPostArgs() + for field in signed_list: + pairs.append((field, data.get('openid.' + field, ''))) + return pairs + + def __repr__(self): + return "<%s.%s %s %s>" % ( + self.__class__.__module__, + self.__class__.__name__, + self.assoc_type, + self.handle) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/consumer/__init__.py b/desktop/core/ext-py/python-openid-2.2.5/openid/consumer/__init__.py new file mode 100644 index 0000000..aab51a2 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/consumer/__init__.py @@ -0,0 +1,6 @@ +""" +This package contains the portions of the library used only when +implementing an OpenID consumer. +""" + +__all__ = ['consumer', 'discover'] diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/consumer/consumer.py b/desktop/core/ext-py/python-openid-2.2.5/openid/consumer/consumer.py new file mode 100644 index 0000000..5205749 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/consumer/consumer.py @@ -0,0 +1,1900 @@ +# -*- test-case-name: openid.test.test_consumer -*- +"""OpenID support for Relying Parties (aka Consumers). + +This module documents the main interface with the OpenID consumer +library. The only part of the library which has to be used and isn't +documented in full here is the store required to create an +C{L{Consumer}} instance. More on the abstract store type and +concrete implementations of it that are provided in the documentation +for the C{L{__init__}} method of the +C{L{Consumer}} class. + + +OVERVIEW +======== + + The OpenID identity verification process most commonly uses the + following steps, as visible to the user of this library: + + 1. The user enters their OpenID into a field on the consumer's + site, and hits a login button. + + 2. The consumer site discovers the user's OpenID provider using + the Yadis protocol. + + 3. The consumer site sends the browser a redirect to the + OpenID provider. This is the authentication request as + described in the OpenID specification. + + 4. The OpenID provider's site sends the browser a redirect + back to the consumer site. This redirect contains the + provider's response to the authentication request. + + The most important part of the flow to note is the consumer's site + must handle two separate HTTP requests in order to perform the + full identity check. + + +LIBRARY DESIGN +============== + + This consumer library is designed with that flow in mind. The + goal is to make it as easy as possible to perform the above steps + securely. + + At a high level, there are two important parts in the consumer + library. The first important part is this module, which contains + the interface to actually use this library. The second is the + C{L{openid.store.interface}} module, which describes the + interface to use if you need to create a custom method for storing + the state this library needs to maintain between requests. + + In general, the second part is less important for users of the + library to know about, as several implementations are provided + which cover a wide variety of situations in which consumers may + use the library. + + This module contains a class, C{L{Consumer}}, with methods + corresponding to the actions necessary in each of steps 2, 3, and + 4 described in the overview. Use of this library should be as easy + as creating an C{L{Consumer}} instance and calling the methods + appropriate for the action the site wants to take. + + +SESSIONS, STORES, AND STATELESS MODE +==================================== + + The C{L{Consumer}} object keeps track of two types of state: + + 1. State of the user's current authentication attempt. Things like + the identity URL, the list of endpoints discovered for that + URL, and in case where some endpoints are unreachable, the list + of endpoints already tried. This state needs to be held from + Consumer.begin() to Consumer.complete(), but it is only applicable + to a single session with a single user agent, and at the end of + the authentication process (i.e. when an OP replies with either + C{id_res} or C{cancel}) it may be discarded. + + 2. State of relationships with servers, i.e. shared secrets + (associations) with servers and nonces seen on signed messages. + This information should persist from one session to the next and + should not be bound to a particular user-agent. + + + These two types of storage are reflected in the first two arguments of + Consumer's constructor, C{session} and C{store}. C{session} is a + dict-like object and we hope your web framework provides you with one + of these bound to the user agent. C{store} is an instance of + L{openid.store.interface.OpenIDStore}. + + Since the store does hold secrets shared between your application and the + OpenID provider, you should be careful about how you use it in a shared + hosting environment. If the filesystem or database permissions of your + web host allow strangers to read from them, do not store your data there! + If you have no safe place to store your data, construct your consumer + with C{None} for the store, and it will operate only in stateless mode. + Stateless mode may be slower, put more load on the OpenID provider, and + trusts the provider to keep you safe from replay attacks. + + + Several store implementation are provided, and the interface is + fully documented so that custom stores can be used as well. See + the documentation for the C{L{Consumer}} class for more + information on the interface for stores. The implementations that + are provided allow the consumer site to store the necessary data + in several different ways, including several SQL databases and + normal files on disk. + + +IMMEDIATE MODE +============== + + In the flow described above, the user may need to confirm to the + OpenID provider that it's ok to disclose his or her identity. + The provider may draw pages asking for information from the user + before it redirects the browser back to the consumer's site. This + is generally transparent to the consumer site, so it is typically + ignored as an implementation detail. + + There can be times, however, where the consumer site wants to get + a response immediately. When this is the case, the consumer can + put the library in immediate mode. In immediate mode, there is an + extra response possible from the server, which is essentially the + server reporting that it doesn't have enough information to answer + the question yet. + + +USING THIS LIBRARY +================== + + Integrating this library into an application is usually a + relatively straightforward process. The process should basically + follow this plan: + + Add an OpenID login field somewhere on your site. When an OpenID + is entered in that field and the form is submitted, it should make + a request to your site which includes that OpenID URL. + + First, the application should L{instantiate a Consumer} + with a session for per-user state and store for shared state. + using the store of choice. + + Next, the application should call the 'C{L{begin}}' method on the + C{L{Consumer}} instance. This method takes the OpenID URL. The + C{L{begin}} method returns an C{L{AuthRequest}} + object. + + Next, the application should call the + C{L{redirectURL}} method on the + C{L{AuthRequest}} object. The parameter C{return_to} is the URL + that the OpenID server will send the user back to after attempting + to verify his or her identity. The C{realm} parameter is the + URL (or URL pattern) that identifies your web site to the user + when he or she is authorizing it. Send a redirect to the + resulting URL to the user's browser. + + That's the first half of the authentication process. The second + half of the process is done after the user's OpenID Provider sends the + user's browser a redirect back to your site to complete their + login. + + When that happens, the user will contact your site at the URL + given as the C{return_to} URL to the + C{L{redirectURL}} call made + above. The request will have several query parameters added to + the URL by the OpenID provider as the information necessary to + finish the request. + + Get a C{L{Consumer}} instance with the same session and store as + before and call its C{L{complete}} method, + passing in all the received query arguments. + + There are multiple possible return types possible from that + method. These indicate whether or not the login was successful, + and include any additional information appropriate for their type. + +@var SUCCESS: constant used as the status for + L{SuccessResponse} objects. + +@var FAILURE: constant used as the status for + L{FailureResponse} objects. + +@var CANCEL: constant used as the status for + L{CancelResponse} objects. + +@var SETUP_NEEDED: constant used as the status for + L{SetupNeededResponse} + objects. +""" + +import cgi +import copy +from urlparse import urlparse, urldefrag + +from openid import fetchers + +from openid.consumer.discover import discover, OpenIDServiceEndpoint, \ + DiscoveryFailure, OPENID_1_0_TYPE, OPENID_1_1_TYPE, OPENID_2_0_TYPE +from openid.message import Message, OPENID_NS, OPENID2_NS, OPENID1_NS, \ + IDENTIFIER_SELECT, no_default, BARE_NS +from openid import cryptutil +from openid import oidutil +from openid.association import Association, default_negotiator, \ + SessionNegotiator +from openid.dh import DiffieHellman +from openid.store.nonce import mkNonce, split as splitNonce +from openid.yadis.manager import Discovery +from openid import urinorm + + +__all__ = ['AuthRequest', 'Consumer', 'SuccessResponse', + 'SetupNeededResponse', 'CancelResponse', 'FailureResponse', + 'SUCCESS', 'FAILURE', 'CANCEL', 'SETUP_NEEDED', + ] + + +def makeKVPost(request_message, server_url): + """Make a Direct Request to an OpenID Provider and return the + result as a Message object. + + @raises openid.fetchers.HTTPFetchingError: if an error is + encountered in making the HTTP post. + + @rtype: L{openid.message.Message} + """ + # XXX: TESTME + resp = fetchers.fetch(server_url, body=request_message.toURLEncoded()) + + # Process response in separate function that can be shared by async code. + return _httpResponseToMessage(resp, server_url) + + +def _httpResponseToMessage(response, server_url): + """Adapt a POST response to a Message. + + @type response: L{openid.fetchers.HTTPResponse} + @param response: Result of a POST to an OpenID endpoint. + + @rtype: L{openid.message.Message} + + @raises openid.fetchers.HTTPFetchingError: if the server returned a + status of other than 200 or 400. + + @raises ServerError: if the server returned an OpenID error. + """ + # Should this function be named Message.fromHTTPResponse instead? + response_message = Message.fromKVForm(response.body) + if response.status == 400: + raise ServerError.fromMessage(response_message) + + elif response.status not in (200, 206): + fmt = 'bad status code from server %s: %s' + error_message = fmt % (server_url, response.status) + raise fetchers.HTTPFetchingError(error_message) + + return response_message + + + +class Consumer(object): + """An OpenID consumer implementation that performs discovery and + does session management. + + @ivar consumer: an instance of an object implementing the OpenID + protocol, but doing no discovery or session management. + + @type consumer: GenericConsumer + + @ivar session: A dictionary-like object representing the user's + session data. This is used for keeping state of the OpenID + transaction when the user is redirected to the server. + + @cvar session_key_prefix: A string that is prepended to session + keys to ensure that they are unique. This variable may be + changed to suit your application. + """ + session_key_prefix = "_openid_consumer_" + + _token = 'last_token' + + _discover = staticmethod(discover) + + def __init__(self, session, store, consumer_class=None): + """Initialize a Consumer instance. + + You should create a new instance of the Consumer object with + every HTTP request that handles OpenID transactions. + + @param session: See L{the session instance variable} + + @param store: an object that implements the interface in + C{L{openid.store.interface.OpenIDStore}}. Several + implementations are provided, to cover common database + environments. + + @type store: C{L{openid.store.interface.OpenIDStore}} + + @see: L{openid.store.interface} + @see: L{openid.store} + """ + self.session = session + if consumer_class is None: + consumer_class = GenericConsumer + self.consumer = consumer_class(store) + self._token_key = self.session_key_prefix + self._token + + def begin(self, user_url, anonymous=False): + """Start the OpenID authentication process. See steps 1-2 in + the overview at the top of this file. + + @param user_url: Identity URL given by the user. This method + performs a textual transformation of the URL to try and + make sure it is normalized. For example, a user_url of + example.com will be normalized to http://example.com/ + normalizing and resolving any redirects the server might + issue. + + @type user_url: unicode + + @param anonymous: Whether to make an anonymous request of the OpenID + provider. Such a request does not ask for an authorization + assertion for an OpenID identifier, but may be used with + extensions to pass other data. e.g. "I don't care who you are, + but I'd like to know your time zone." + + @type anonymous: bool + + @returns: An object containing the discovered information will + be returned, with a method for building a redirect URL to + the server, as described in step 3 of the overview. This + object may also be used to add extension arguments to the + request, using its + L{addExtensionArg} + method. + + @returntype: L{AuthRequest} + + @raises openid.consumer.discover.DiscoveryFailure: when I fail to + find an OpenID server for this URL. If the C{yadis} package + is available, L{openid.consumer.discover.DiscoveryFailure} is + an alias for C{yadis.discover.DiscoveryFailure}. + """ + disco = Discovery(self.session, user_url, self.session_key_prefix) + try: + service = disco.getNextService(self._discover) + except fetchers.HTTPFetchingError, why: + raise DiscoveryFailure( + 'Error fetching XRDS document: %s' % (why[0],), None) + + if service is None: + raise DiscoveryFailure( + 'No usable OpenID services found for %s' % (user_url,), None) + else: + return self.beginWithoutDiscovery(service, anonymous) + + def beginWithoutDiscovery(self, service, anonymous=False): + """Start OpenID verification without doing OpenID server + discovery. This method is used internally by Consumer.begin + after discovery is performed, and exists to provide an + interface for library users needing to perform their own + discovery. + + @param service: an OpenID service endpoint descriptor. This + object and factories for it are found in the + L{openid.consumer.discover} module. + + @type service: + L{OpenIDServiceEndpoint} + + @returns: an OpenID authentication request object. + + @rtype: L{AuthRequest} + + @See: Openid.consumer.consumer.Consumer.begin + @see: openid.consumer.discover + """ + auth_req = self.consumer.begin(service) + self.session[self._token_key] = auth_req.endpoint + + try: + auth_req.setAnonymous(anonymous) + except ValueError, why: + raise ProtocolError(str(why)) + + return auth_req + + def complete(self, query, current_url): + """Called to interpret the server's response to an OpenID + request. It is called in step 4 of the flow described in the + consumer overview. + + @param query: A dictionary of the query parameters for this + HTTP request. + + @param current_url: The URL used to invoke the application. + Extract the URL from your application's web + request framework and specify it here to have it checked + against the openid.return_to value in the response. If + the return_to URL check fails, the status of the + completion will be FAILURE. + + @returns: a subclass of Response. The type of response is + indicated by the status attribute, which will be one of + SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED. + + @see: L{SuccessResponse} + @see: L{CancelResponse} + @see: L{SetupNeededResponse} + @see: L{FailureResponse} + """ + + endpoint = self.session.get(self._token_key) + + message = Message.fromPostArgs(query) + response = self.consumer.complete(message, endpoint, current_url) + + try: + del self.session[self._token_key] + except KeyError: + pass + + if (response.status in ['success', 'cancel'] and + response.identity_url is not None): + + disco = Discovery(self.session, + response.identity_url, + self.session_key_prefix) + # This is OK to do even if we did not do discovery in + # the first place. + disco.cleanup(force=True) + + return response + + def setAssociationPreference(self, association_preferences): + """Set the order in which association types/sessions should be + attempted. For instance, to only allow HMAC-SHA256 + associations created with a DH-SHA256 association session: + + >>> consumer.setAssociationPreference([('HMAC-SHA256', 'DH-SHA256')]) + + Any association type/association type pair that is not in this + list will not be attempted at all. + + @param association_preferences: The list of allowed + (association type, association session type) pairs that + should be allowed for this consumer to use, in order from + most preferred to least preferred. + @type association_preferences: [(str, str)] + + @returns: None + + @see: C{L{openid.association.SessionNegotiator}} + """ + self.consumer.negotiator = SessionNegotiator(association_preferences) + +class DiffieHellmanSHA1ConsumerSession(object): + session_type = 'DH-SHA1' + hash_func = staticmethod(cryptutil.sha1) + secret_size = 20 + allowed_assoc_types = ['HMAC-SHA1'] + + def __init__(self, dh=None): + if dh is None: + dh = DiffieHellman.fromDefaults() + + self.dh = dh + + def getRequest(self): + cpub = cryptutil.longToBase64(self.dh.public) + + args = {'dh_consumer_public': cpub} + + if not self.dh.usingDefaultValues(): + args.update({ + 'dh_modulus': cryptutil.longToBase64(self.dh.modulus), + 'dh_gen': cryptutil.longToBase64(self.dh.generator), + }) + + return args + + def extractSecret(self, response): + dh_server_public64 = response.getArg( + OPENID_NS, 'dh_server_public', no_default) + enc_mac_key64 = response.getArg(OPENID_NS, 'enc_mac_key', no_default) + dh_server_public = cryptutil.base64ToLong(dh_server_public64) + enc_mac_key = oidutil.fromBase64(enc_mac_key64) + return self.dh.xorSecret(dh_server_public, enc_mac_key, self.hash_func) + +class DiffieHellmanSHA256ConsumerSession(DiffieHellmanSHA1ConsumerSession): + session_type = 'DH-SHA256' + hash_func = staticmethod(cryptutil.sha256) + secret_size = 32 + allowed_assoc_types = ['HMAC-SHA256'] + +class PlainTextConsumerSession(object): + session_type = 'no-encryption' + allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256'] + + def getRequest(self): + return {} + + def extractSecret(self, response): + mac_key64 = response.getArg(OPENID_NS, 'mac_key', no_default) + return oidutil.fromBase64(mac_key64) + +class SetupNeededError(Exception): + """Internally-used exception that indicates that an immediate-mode + request cancelled.""" + def __init__(self, user_setup_url=None): + Exception.__init__(self, user_setup_url) + self.user_setup_url = user_setup_url + +class ProtocolError(ValueError): + """Exception that indicates that a message violated the + protocol. It is raised and caught internally to this file.""" + +class TypeURIMismatch(ProtocolError): + """A protocol error arising from type URIs mismatching + """ + + def __init__(self, expected, endpoint): + ProtocolError.__init__(self, expected, endpoint) + self.expected = expected + self.endpoint = endpoint + + def __str__(self): + s = '<%s.%s: Required type %s not found in %s for endpoint %s>' % ( + self.__class__.__module__, self.__class__.__name__, + self.expected, self.endpoint.type_uris, self.endpoint) + return s + + + +class ServerError(Exception): + """Exception that is raised when the server returns a 400 response + code to a direct request.""" + + def __init__(self, error_text, error_code, message): + Exception.__init__(self, error_text) + self.error_text = error_text + self.error_code = error_code + self.message = message + + def fromMessage(cls, message): + """Generate a ServerError instance, extracting the error text + and the error code from the message.""" + error_text = message.getArg( + OPENID_NS, 'error', '') + error_code = message.getArg(OPENID_NS, 'error_code') + return cls(error_text, error_code, message) + + fromMessage = classmethod(fromMessage) + +class GenericConsumer(object): + """This is the implementation of the common logic for OpenID + consumers. It is unaware of the application in which it is + running. + + @ivar negotiator: An object that controls the kind of associations + that the consumer makes. It defaults to + C{L{openid.association.default_negotiator}}. Assign a + different negotiator to it if you have specific requirements + for how associations are made. + @type negotiator: C{L{openid.association.SessionNegotiator}} + """ + + # The name of the query parameter that gets added to the return_to + # URL when using OpenID1. You can change this value if you want or + # need a different name, but don't make it start with openid, + # because it's not a standard protocol thing for OpenID1. For + # OpenID2, the library will take care of the nonce using standard + # OpenID query parameter names. + openid1_nonce_query_arg_name = 'janrain_nonce' + + # Another query parameter that gets added to the return_to for + # OpenID 1; if the user's session state is lost, use this claimed + # identifier to do discovery when verifying the response. + openid1_return_to_identifier_name = 'openid1_claimed_id' + + session_types = { + 'DH-SHA1':DiffieHellmanSHA1ConsumerSession, + 'DH-SHA256':DiffieHellmanSHA256ConsumerSession, + 'no-encryption':PlainTextConsumerSession, + } + + _discover = staticmethod(discover) + + def __init__(self, store): + self.store = store + self.negotiator = default_negotiator.copy() + + def begin(self, service_endpoint): + """Create an AuthRequest object for the specified + service_endpoint. This method will create an association if + necessary.""" + if self.store is None: + assoc = None + else: + assoc = self._getAssociation(service_endpoint) + + request = AuthRequest(service_endpoint, assoc) + request.return_to_args[self.openid1_nonce_query_arg_name] = mkNonce() + + if request.message.isOpenID1(): + request.return_to_args[self.openid1_return_to_identifier_name] = \ + request.endpoint.claimed_id + + return request + + def complete(self, message, endpoint, return_to): + """Process the OpenID message, using the specified endpoint + and return_to URL as context. This method will handle any + OpenID message that is sent to the return_to URL. + """ + mode = message.getArg(OPENID_NS, 'mode', '') + + modeMethod = getattr(self, '_complete_' + mode, + self._completeInvalid) + + return modeMethod(message, endpoint, return_to) + + def _complete_cancel(self, message, endpoint, _): + return CancelResponse(endpoint) + + def _complete_error(self, message, endpoint, _): + error = message.getArg(OPENID_NS, 'error') + contact = message.getArg(OPENID_NS, 'contact') + reference = message.getArg(OPENID_NS, 'reference') + + return FailureResponse(endpoint, error, contact=contact, + reference=reference) + + def _complete_setup_needed(self, message, endpoint, _): + if not message.isOpenID2(): + return self._completeInvalid(message, endpoint, _) + + user_setup_url = message.getArg(OPENID2_NS, 'user_setup_url') + return SetupNeededResponse(endpoint, user_setup_url) + + def _complete_id_res(self, message, endpoint, return_to): + try: + self._checkSetupNeeded(message) + except SetupNeededError, why: + return SetupNeededResponse(endpoint, why.user_setup_url) + else: + try: + return self._doIdRes(message, endpoint, return_to) + except (ProtocolError, DiscoveryFailure), why: + return FailureResponse(endpoint, why[0]) + + def _completeInvalid(self, message, endpoint, _): + mode = message.getArg(OPENID_NS, 'mode', '') + return FailureResponse(endpoint, + 'Invalid openid.mode: %r' % (mode,)) + + def _checkReturnTo(self, message, return_to): + """Check an OpenID message and its openid.return_to value + against a return_to URL from an application. Return True on + success, False on failure. + """ + # Check the openid.return_to args against args in the original + # message. + try: + self._verifyReturnToArgs(message.toPostArgs()) + except ProtocolError, why: + oidutil.log("Verifying return_to arguments: %s" % (why[0],)) + return False + + # Check the return_to base URL against the one in the message. + msg_return_to = message.getArg(OPENID_NS, 'return_to') + + # The URL scheme, authority, and path MUST be the same between + # the two URLs. + app_parts = urlparse(urinorm.urinorm(return_to)) + msg_parts = urlparse(urinorm.urinorm(msg_return_to)) + + # (addressing scheme, network location, path) must be equal in + # both URLs. + for part in range(0, 3): + if app_parts[part] != msg_parts[part]: + return False + + return True + + _makeKVPost = staticmethod(makeKVPost) + + def _checkSetupNeeded(self, message): + """Check an id_res message to see if it is a + checkid_immediate cancel response. + + @raises SetupNeededError: if it is a checkid_immediate cancellation + """ + # In OpenID 1, we check to see if this is a cancel from + # immediate mode by the presence of the user_setup_url + # parameter. + if message.isOpenID1(): + user_setup_url = message.getArg(OPENID1_NS, 'user_setup_url') + if user_setup_url is not None: + raise SetupNeededError(user_setup_url) + + def _doIdRes(self, message, endpoint, return_to): + """Handle id_res responses that are not cancellations of + immediate mode requests. + + @param message: the response paramaters. + @param endpoint: the discovered endpoint object. May be None. + + @raises ProtocolError: If the message contents are not + well-formed according to the OpenID specification. This + includes missing fields or not signing fields that should + be signed. + + @raises DiscoveryFailure: If the subject of the id_res message + does not match the supplied endpoint, and discovery on the + identifier in the message fails (this should only happen + when using OpenID 2) + + @returntype: L{Response} + """ + # Checks for presence of appropriate fields (and checks + # signed list fields) + self._idResCheckForFields(message) + + if not self._checkReturnTo(message, return_to): + raise ProtocolError( + "return_to does not match return URL. Expected %r, got %r" + % (return_to, message.getArg(OPENID_NS, 'return_to'))) + + + # Verify discovery information: + endpoint = self._verifyDiscoveryResults(message, endpoint) + oidutil.log("Received id_res response from %s using association %s" % + (endpoint.server_url, + message.getArg(OPENID_NS, 'assoc_handle'))) + + self._idResCheckSignature(message, endpoint.server_url) + + # Will raise a ProtocolError if the nonce is bad + self._idResCheckNonce(message, endpoint) + + signed_list_str = message.getArg(OPENID_NS, 'signed', no_default) + signed_list = signed_list_str.split(',') + signed_fields = ["openid." + s for s in signed_list] + return SuccessResponse(endpoint, message, signed_fields) + + def _idResGetNonceOpenID1(self, message, endpoint): + """Extract the nonce from an OpenID 1 response. Return the + nonce from the BARE_NS since we independently check the + return_to arguments are the same as those in the response + message. + + See the openid1_nonce_query_arg_name class variable + + @returns: The nonce as a string or None + """ + return message.getArg(BARE_NS, self.openid1_nonce_query_arg_name) + + def _idResCheckNonce(self, message, endpoint): + if message.isOpenID1(): + # This indicates that the nonce was generated by the consumer + nonce = self._idResGetNonceOpenID1(message, endpoint) + server_url = '' + else: + nonce = message.getArg(OPENID2_NS, 'response_nonce') + server_url = endpoint.server_url + + if nonce is None: + raise ProtocolError('Nonce missing from response') + + try: + timestamp, salt = splitNonce(nonce) + except ValueError, why: + raise ProtocolError('Malformed nonce: %s' % (why[0],)) + + if (self.store is not None and + not self.store.useNonce(server_url, timestamp, salt)): + raise ProtocolError('Nonce already used or out of range') + + def _idResCheckSignature(self, message, server_url): + assoc_handle = message.getArg(OPENID_NS, 'assoc_handle') + if self.store is None: + assoc = None + else: + assoc = self.store.getAssociation(server_url, assoc_handle) + + if assoc: + if assoc.getExpiresIn() <= 0: + # XXX: It might be a good idea sometimes to re-start the + # authentication with a new association. Doing it + # automatically opens the possibility for + # denial-of-service by a server that just returns expired + # associations (or really short-lived associations) + raise ProtocolError( + 'Association with %s expired' % (server_url,)) + + if not assoc.checkMessageSignature(message): + raise ProtocolError('Bad signature') + + else: + # It's not an association we know about. Stateless mode is our + # only possible path for recovery. + # XXX - async framework will not want to block on this call to + # _checkAuth. + if not self._checkAuth(message, server_url): + raise ProtocolError('Server denied check_authentication') + + def _idResCheckForFields(self, message): + # XXX: this should be handled by the code that processes the + # response (that is, if a field is missing, we should not have + # to explicitly check that it's present, just make sure that + # the fields are actually being used by the rest of the code + # in tests). Although, which fields are signed does need to be + # checked somewhere. + basic_fields = ['return_to', 'assoc_handle', 'sig', 'signed'] + basic_sig_fields = ['return_to', 'identity'] + + require_fields = { + OPENID2_NS: basic_fields + ['op_endpoint'], + OPENID1_NS: basic_fields + ['identity'], + } + + require_sigs = { + OPENID2_NS: basic_sig_fields + ['response_nonce', + 'claimed_id', + 'assoc_handle', + 'op_endpoint',], + OPENID1_NS: basic_sig_fields, + } + + for field in require_fields[message.getOpenIDNamespace()]: + if not message.hasKey(OPENID_NS, field): + raise ProtocolError('Missing required field %r' % (field,)) + + signed_list_str = message.getArg(OPENID_NS, 'signed', no_default) + signed_list = signed_list_str.split(',') + + for field in require_sigs[message.getOpenIDNamespace()]: + # Field is present and not in signed list + if message.hasKey(OPENID_NS, field) and field not in signed_list: + raise ProtocolError('"%s" not signed' % (field,)) + + + def _verifyReturnToArgs(query): + """Verify that the arguments in the return_to URL are present in this + response. + """ + message = Message.fromPostArgs(query) + return_to = message.getArg(OPENID_NS, 'return_to') + + if return_to is None: + raise ProtocolError('Response has no return_to') + + parsed_url = urlparse(return_to) + rt_query = parsed_url[4] + parsed_args = cgi.parse_qsl(rt_query) + + for rt_key, rt_value in parsed_args: + try: + value = query[rt_key] + if rt_value != value: + format = ("parameter %s value %r does not match " + "return_to's value %r") + raise ProtocolError(format % (rt_key, value, rt_value)) + except KeyError: + format = "return_to parameter %s absent from query %r" + raise ProtocolError(format % (rt_key, query)) + + # Make sure all non-OpenID arguments in the response are also + # in the signed return_to. + bare_args = message.getArgs(BARE_NS) + for pair in bare_args.iteritems(): + if pair not in parsed_args: + raise ProtocolError("Parameter %s not in return_to URL" % (pair[0],)) + + _verifyReturnToArgs = staticmethod(_verifyReturnToArgs) + + def _verifyDiscoveryResults(self, resp_msg, endpoint=None): + """ + Extract the information from an OpenID assertion message and + verify it against the original + + @param endpoint: The endpoint that resulted from doing discovery + @param resp_msg: The id_res message object + + @returns: the verified endpoint + """ + if resp_msg.getOpenIDNamespace() == OPENID2_NS: + return self._verifyDiscoveryResultsOpenID2(resp_msg, endpoint) + else: + return self._verifyDiscoveryResultsOpenID1(resp_msg, endpoint) + + + def _verifyDiscoveryResultsOpenID2(self, resp_msg, endpoint): + to_match = OpenIDServiceEndpoint() + to_match.type_uris = [OPENID_2_0_TYPE] + to_match.claimed_id = resp_msg.getArg(OPENID2_NS, 'claimed_id') + to_match.local_id = resp_msg.getArg(OPENID2_NS, 'identity') + + # Raises a KeyError when the op_endpoint is not present + to_match.server_url = resp_msg.getArg( + OPENID2_NS, 'op_endpoint', no_default) + + # claimed_id and identifier must both be present or both + # be absent + if (to_match.claimed_id is None and + to_match.local_id is not None): + raise ProtocolError( + 'openid.identity is present without openid.claimed_id') + + elif (to_match.claimed_id is not None and + to_match.local_id is None): + raise ProtocolError( + 'openid.claimed_id is present without openid.identity') + + # This is a response without identifiers, so there's really no + # checking that we can do, so return an endpoint that's for + # the specified `openid.op_endpoint' + elif to_match.claimed_id is None: + return OpenIDServiceEndpoint.fromOPEndpointURL(to_match.server_url) + + # The claimed ID doesn't match, so we have to do discovery + # again. This covers not using sessions, OP identifier + # endpoints and responses that didn't match the original + # request. + if not endpoint: + oidutil.log('No pre-discovered information supplied.') + endpoint = self._discoverAndVerify(to_match.claimed_id, [to_match]) + else: + # The claimed ID matches, so we use the endpoint that we + # discovered in initiation. This should be the most common + # case. + try: + self._verifyDiscoverySingle(endpoint, to_match) + except ProtocolError, e: + oidutil.log( + "Error attempting to use stored discovery information: " + + str(e)) + oidutil.log("Attempting discovery to verify endpoint") + endpoint = self._discoverAndVerify( + to_match.claimed_id, [to_match]) + + # The endpoint we return should have the claimed ID from the + # message we just verified, fragment and all. + if endpoint.claimed_id != to_match.claimed_id: + endpoint = copy.copy(endpoint) + endpoint.claimed_id = to_match.claimed_id + return endpoint + + def _verifyDiscoveryResultsOpenID1(self, resp_msg, endpoint): + claimed_id = resp_msg.getArg(BARE_NS, self.openid1_return_to_identifier_name) + + if endpoint is None and claimed_id is None: + raise RuntimeError( + 'When using OpenID 1, the claimed ID must be supplied, ' + 'either by passing it through as a return_to parameter ' + 'or by using a session, and supplied to the GenericConsumer ' + 'as the argument to complete()') + elif endpoint is not None and claimed_id is None: + claimed_id = endpoint.claimed_id + + to_match = OpenIDServiceEndpoint() + to_match.type_uris = [OPENID_1_1_TYPE] + to_match.local_id = resp_msg.getArg(OPENID1_NS, 'identity') + # Restore delegate information from the initiation phase + to_match.claimed_id = claimed_id + + if to_match.local_id is None: + raise ProtocolError('Missing required field openid.identity') + + to_match_1_0 = copy.copy(to_match) + to_match_1_0.type_uris = [OPENID_1_0_TYPE] + + if endpoint is not None: + try: + try: + self._verifyDiscoverySingle(endpoint, to_match) + except TypeURIMismatch: + self._verifyDiscoverySingle(endpoint, to_match_1_0) + except ProtocolError, e: + oidutil.log("Error attempting to use stored discovery information: " + + str(e)) + oidutil.log("Attempting discovery to verify endpoint") + else: + return endpoint + + # Endpoint is either bad (failed verification) or None + return self._discoverAndVerify(claimed_id, [to_match, to_match_1_0]) + + def _verifyDiscoverySingle(self, endpoint, to_match): + """Verify that the given endpoint matches the information + extracted from the OpenID assertion, and raise an exception if + there is a mismatch. + + @type endpoint: openid.consumer.discover.OpenIDServiceEndpoint + @type to_match: openid.consumer.discover.OpenIDServiceEndpoint + + @rtype: NoneType + + @raises ProtocolError: when the endpoint does not match the + discovered information. + """ + # Every type URI that's in the to_match endpoint has to be + # present in the discovered endpoint. + for type_uri in to_match.type_uris: + if not endpoint.usesExtension(type_uri): + raise TypeURIMismatch(type_uri, endpoint) + + # Fragments do not influence discovery, so we can't compare a + # claimed identifier with a fragment to discovered information. + defragged_claimed_id, _ = urldefrag(to_match.claimed_id) + if defragged_claimed_id != endpoint.claimed_id: + raise ProtocolError( + 'Claimed ID does not match (different subjects!), ' + 'Expected %s, got %s' % + (defragged_claimed_id, endpoint.claimed_id)) + + if to_match.getLocalID() != endpoint.getLocalID(): + raise ProtocolError('local_id mismatch. Expected %s, got %s' % + (to_match.getLocalID(), endpoint.getLocalID())) + + # If the server URL is None, this must be an OpenID 1 + # response, because op_endpoint is a required parameter in + # OpenID 2. In that case, we don't actually care what the + # discovered server_url is, because signature checking or + # check_auth should take care of that check for us. + if to_match.server_url is None: + assert to_match.preferredNamespace() == OPENID1_NS, ( + """The code calling this must ensure that OpenID 2 + responses have a non-none `openid.op_endpoint' and + that it is set as the `server_url' attribute of the + `to_match' endpoint.""") + + elif to_match.server_url != endpoint.server_url: + raise ProtocolError('OP Endpoint mismatch. Expected %s, got %s' % + (to_match.server_url, endpoint.server_url)) + + def _discoverAndVerify(self, claimed_id, to_match_endpoints): + """Given an endpoint object created from the information in an + OpenID response, perform discovery and verify the discovery + results, returning the matching endpoint that is the result of + doing that discovery. + + @type to_match: openid.consumer.discover.OpenIDServiceEndpoint + @param to_match: The endpoint whose information we're confirming + + @rtype: openid.consumer.discover.OpenIDServiceEndpoint + @returns: The result of performing discovery on the claimed + identifier in `to_match' + + @raises DiscoveryFailure: when discovery fails. + """ + oidutil.log('Performing discovery on %s' % (claimed_id,)) + _, services = self._discover(claimed_id) + if not services: + raise DiscoveryFailure('No OpenID information found at %s' % + (claimed_id,), None) + return self._verifyDiscoveredServices(claimed_id, services, + to_match_endpoints) + + + def _verifyDiscoveredServices(self, claimed_id, services, to_match_endpoints): + """See @L{_discoverAndVerify}""" + + # Search the services resulting from discovery to find one + # that matches the information from the assertion + failure_messages = [] + for endpoint in services: + for to_match_endpoint in to_match_endpoints: + try: + self._verifyDiscoverySingle( + endpoint, to_match_endpoint) + except ProtocolError, why: + failure_messages.append(str(why)) + else: + # It matches, so discover verification has + # succeeded. Return this endpoint. + return endpoint + else: + oidutil.log('Discovery verification failure for %s' % + (claimed_id,)) + for failure_message in failure_messages: + oidutil.log(' * Endpoint mismatch: ' + failure_message) + + raise DiscoveryFailure( + 'No matching endpoint found after discovering %s' + % (claimed_id,), None) + + def _checkAuth(self, message, server_url): + """Make a check_authentication request to verify this message. + + @returns: True if the request is valid. + @rtype: bool + """ + oidutil.log('Using OpenID check_authentication') + request = self._createCheckAuthRequest(message) + if request is None: + return False + try: + response = self._makeKVPost(request, server_url) + except (fetchers.HTTPFetchingError, ServerError), e: + oidutil.log('check_authentication failed: %s' % (e[0],)) + return False + else: + return self._processCheckAuthResponse(response, server_url) + + def _createCheckAuthRequest(self, message): + """Generate a check_authentication request message given an + id_res message. + """ + signed = message.getArg(OPENID_NS, 'signed') + if signed: + for k in signed.split(','): + oidutil.log(k) + val = message.getAliasedArg(k) + + # Signed value is missing + if val is None: + oidutil.log('Missing signed field %r' % (k,)) + return None + + check_auth_message = message.copy() + check_auth_message.setArg(OPENID_NS, 'mode', 'check_authentication') + return check_auth_message + + def _processCheckAuthResponse(self, response, server_url): + """Process the response message from a check_authentication + request, invalidating associations if requested. + """ + is_valid = response.getArg(OPENID_NS, 'is_valid', 'false') + + invalidate_handle = response.getArg(OPENID_NS, 'invalidate_handle') + if invalidate_handle is not None: + oidutil.log( + 'Received "invalidate_handle" from server %s' % (server_url,)) + if self.store is None: + oidutil.log('Unexpectedly got invalidate_handle without ' + 'a store!') + else: + self.store.removeAssociation(server_url, invalidate_handle) + + if is_valid == 'true': + return True + else: + oidutil.log('Server responds that checkAuth call is not valid') + return False + + def _getAssociation(self, endpoint): + """Get an association for the endpoint's server_url. + + First try seeing if we have a good association in the + store. If we do not, then attempt to negotiate an association + with the server. + + If we negotiate a good association, it will get stored. + + @returns: A valid association for the endpoint's server_url or None + @rtype: openid.association.Association or NoneType + """ + assoc = self.store.getAssociation(endpoint.server_url) + + if assoc is None or assoc.expiresIn <= 0: + assoc = self._negotiateAssociation(endpoint) + if assoc is not None: + self.store.storeAssociation(endpoint.server_url, assoc) + + return assoc + + def _negotiateAssociation(self, endpoint): + """Make association requests to the server, attempting to + create a new association. + + @returns: a new association object + + @rtype: L{openid.association.Association} + """ + # Get our preferred session/association type from the negotiatior. + assoc_type, session_type = self.negotiator.getAllowedType() + + try: + assoc = self._requestAssociation( + endpoint, assoc_type, session_type) + except ServerError, why: + supportedTypes = self._extractSupportedAssociationType(why, + endpoint, + assoc_type) + if supportedTypes is not None: + assoc_type, session_type = supportedTypes + # Attempt to create an association from the assoc_type + # and session_type that the server told us it + # supported. + try: + assoc = self._requestAssociation( + endpoint, assoc_type, session_type) + except ServerError, why: + # Do not keep trying, since it rejected the + # association type that it told us to use. + oidutil.log('Server %s refused its suggested association ' + 'type: session_type=%s, assoc_type=%s' + % (endpoint.server_url, session_type, + assoc_type)) + return None + else: + return assoc + else: + return assoc + + def _extractSupportedAssociationType(self, server_error, endpoint, + assoc_type): + """Handle ServerErrors resulting from association requests. + + @returns: If server replied with an C{unsupported-type} error, + return a tuple of supported C{association_type}, C{session_type}. + Otherwise logs the error and returns None. + @rtype: tuple or None + """ + # Any error message whose code is not 'unsupported-type' + # should be considered a total failure. + if server_error.error_code != 'unsupported-type' or \ + server_error.message.isOpenID1(): + oidutil.log( + 'Server error when requesting an association from %r: %s' + % (endpoint.server_url, server_error.error_text)) + return None + + # The server didn't like the association/session type + # that we sent, and it sent us back a message that + # might tell us how to handle it. + oidutil.log( + 'Unsupported association type %s: %s' % (assoc_type, + server_error.error_text,)) + + # Extract the session_type and assoc_type from the + # error message + assoc_type = server_error.message.getArg(OPENID_NS, 'assoc_type') + session_type = server_error.message.getArg(OPENID_NS, 'session_type') + + if assoc_type is None or session_type is None: + oidutil.log('Server responded with unsupported association ' + 'session but did not supply a fallback.') + return None + elif not self.negotiator.isAllowed(assoc_type, session_type): + fmt = ('Server sent unsupported session/association type: ' + 'session_type=%s, assoc_type=%s') + oidutil.log(fmt % (session_type, assoc_type)) + return None + else: + return assoc_type, session_type + + + def _requestAssociation(self, endpoint, assoc_type, session_type): + """Make and process one association request to this endpoint's + OP endpoint URL. + + @returns: An association object or None if the association + processing failed. + + @raises ServerError: when the remote OpenID server returns an error. + """ + assoc_session, args = self._createAssociateRequest( + endpoint, assoc_type, session_type) + + try: + response = self._makeKVPost(args, endpoint.server_url) + except fetchers.HTTPFetchingError, why: + oidutil.log('openid.associate request failed: %s' % (why[0],)) + return None + + try: + assoc = self._extractAssociation(response, assoc_session) + except KeyError, why: + oidutil.log('Missing required parameter in response from %s: %s' + % (endpoint.server_url, why[0])) + return None + except ProtocolError, why: + oidutil.log('Protocol error parsing response from %s: %s' % ( + endpoint.server_url, why[0])) + return None + else: + return assoc + + def _createAssociateRequest(self, endpoint, assoc_type, session_type): + """Create an association request for the given assoc_type and + session_type. + + @param endpoint: The endpoint whose server_url will be + queried. The important bit about the endpoint is whether + it's in compatiblity mode (OpenID 1.1) + + @param assoc_type: The association type that the request + should ask for. + @type assoc_type: str + + @param session_type: The session type that should be used in + the association request. The session_type is used to + create an association session object, and that session + object is asked for any additional fields that it needs to + add to the request. + @type session_type: str + + @returns: a pair of the association session object and the + request message that will be sent to the server. + @rtype: (association session type (depends on session_type), + openid.message.Message) + """ + session_type_class = self.session_types[session_type] + assoc_session = session_type_class() + + args = { + 'mode': 'associate', + 'assoc_type': assoc_type, + } + + if not endpoint.compatibilityMode(): + args['ns'] = OPENID2_NS + + # Leave out the session type if we're in compatibility mode + # *and* it's no-encryption. + if (not endpoint.compatibilityMode() or + assoc_session.session_type != 'no-encryption'): + args['session_type'] = assoc_session.session_type + + args.update(assoc_session.getRequest()) + message = Message.fromOpenIDArgs(args) + return assoc_session, message + + def _getOpenID1SessionType(self, assoc_response): + """Given an association response message, extract the OpenID + 1.X session type. + + This function mostly takes care of the 'no-encryption' default + behavior in OpenID 1. + + If the association type is plain-text, this function will + return 'no-encryption' + + @returns: The association type for this message + @rtype: str + + @raises KeyError: when the session_type field is absent. + """ + # If it's an OpenID 1 message, allow session_type to default + # to None (which signifies "no-encryption") + session_type = assoc_response.getArg(OPENID1_NS, 'session_type') + + # Handle the differences between no-encryption association + # respones in OpenID 1 and 2: + + # no-encryption is not really a valid session type for + # OpenID 1, but we'll accept it anyway, while issuing a + # warning. + if session_type == 'no-encryption': + oidutil.log('WARNING: OpenID server sent "no-encryption"' + 'for OpenID 1.X') + + # Missing or empty session type is the way to flag a + # 'no-encryption' response. Change the session type to + # 'no-encryption' so that it can be handled in the same + # way as OpenID 2 'no-encryption' respones. + elif session_type == '' or session_type is None: + session_type = 'no-encryption' + + return session_type + + def _extractAssociation(self, assoc_response, assoc_session): + """Attempt to extract an association from the response, given + the association response message and the established + association session. + + @param assoc_response: The association response message from + the server + @type assoc_response: openid.message.Message + + @param assoc_session: The association session object that was + used when making the request + @type assoc_session: depends on the session type of the request + + @raises ProtocolError: when data is malformed + @raises KeyError: when a field is missing + + @rtype: openid.association.Association + """ + # Extract the common fields from the response, raising an + # exception if they are not found + assoc_type = assoc_response.getArg( + OPENID_NS, 'assoc_type', no_default) + assoc_handle = assoc_response.getArg( + OPENID_NS, 'assoc_handle', no_default) + + # expires_in is a base-10 string. The Python parsing will + # accept literals that have whitespace around them and will + # accept negative values. Neither of these are really in-spec, + # but we think it's OK to accept them. + expires_in_str = assoc_response.getArg( + OPENID_NS, 'expires_in', no_default) + try: + expires_in = int(expires_in_str) + except ValueError, why: + raise ProtocolError('Invalid expires_in field: %s' % (why[0],)) + + # OpenID 1 has funny association session behaviour. + if assoc_response.isOpenID1(): + session_type = self._getOpenID1SessionType(assoc_response) + else: + session_type = assoc_response.getArg( + OPENID2_NS, 'session_type', no_default) + + # Session type mismatch + if assoc_session.session_type != session_type: + if (assoc_response.isOpenID1() and + session_type == 'no-encryption'): + # In OpenID 1, any association request can result in a + # 'no-encryption' association response. Setting + # assoc_session to a new no-encryption session should + # make the rest of this function work properly for + # that case. + assoc_session = PlainTextConsumerSession() + else: + # Any other mismatch, regardless of protocol version + # results in the failure of the association session + # altogether. + fmt = 'Session type mismatch. Expected %r, got %r' + message = fmt % (assoc_session.session_type, session_type) + raise ProtocolError(message) + + # Make sure assoc_type is valid for session_type + if assoc_type not in assoc_session.allowed_assoc_types: + fmt = 'Unsupported assoc_type for session %s returned: %s' + raise ProtocolError(fmt % (assoc_session.session_type, assoc_type)) + + # Delegate to the association session to extract the secret + # from the response, however is appropriate for that session + # type. + try: + secret = assoc_session.extractSecret(assoc_response) + except ValueError, why: + fmt = 'Malformed response for %s session: %s' + raise ProtocolError(fmt % (assoc_session.session_type, why[0])) + + return Association.fromExpiresIn( + expires_in, assoc_handle, secret, assoc_type) + +class AuthRequest(object): + """An object that holds the state necessary for generating an + OpenID authentication request. This object holds the association + with the server and the discovered information with which the + request will be made. + + It is separate from the consumer because you may wish to add + things to the request before sending it on its way to the + server. It also has serialization options that let you encode the + authentication request as a URL or as a form POST. + """ + + def __init__(self, endpoint, assoc): + """ + Creates a new AuthRequest object. This just stores each + argument in an appropriately named field. + + Users of this library should not create instances of this + class. Instances of this class are created by the library + when needed. + """ + self.assoc = assoc + self.endpoint = endpoint + self.return_to_args = {} + self.message = Message(endpoint.preferredNamespace()) + self._anonymous = False + + def setAnonymous(self, is_anonymous): + """Set whether this request should be made anonymously. If a + request is anonymous, the identifier will not be sent in the + request. This is only useful if you are making another kind of + request with an extension in this request. + + Anonymous requests are not allowed when the request is made + with OpenID 1. + + @raises ValueError: when attempting to set an OpenID1 request + as anonymous + """ + if is_anonymous and self.message.isOpenID1(): + raise ValueError('OpenID 1 requests MUST include the ' + 'identifier in the request') + else: + self._anonymous = is_anonymous + + def addExtension(self, extension_request): + """Add an extension to this checkid request. + + @param extension_request: An object that implements the + extension interface for adding arguments to an OpenID + message. + """ + extension_request.toMessage(self.message) + + def addExtensionArg(self, namespace, key, value): + """Add an extension argument to this OpenID authentication + request. + + Use caution when adding arguments, because they will be + URL-escaped and appended to the redirect URL, which can easily + get quite long. + + @param namespace: The namespace for the extension. For + example, the simple registration extension uses the + namespace C{sreg}. + + @type namespace: str + + @param key: The key within the extension namespace. For + example, the nickname field in the simple registration + extension's key is C{nickname}. + + @type key: str + + @param value: The value to provide to the server for this + argument. + + @type value: str + """ + self.message.setArg(namespace, key, value) + + def getMessage(self, realm, return_to=None, immediate=False): + """Produce a L{openid.message.Message} representing this request. + + @param realm: The URL (or URL pattern) that identifies your + web site to the user when she is authorizing it. + + @type realm: str + + @param return_to: The URL that the OpenID provider will send the + user back to after attempting to verify her identity. + + Not specifying a return_to URL means that the user will not + be returned to the site issuing the request upon its + completion. + + @type return_to: str + + @param immediate: If True, the OpenID provider is to send back + a response immediately, useful for behind-the-scenes + authentication attempts. Otherwise the OpenID provider + may engage the user before providing a response. This is + the default case, as the user may need to provide + credentials or approve the request before a positive + response can be sent. + + @type immediate: bool + + @returntype: L{openid.message.Message} + """ + if return_to: + return_to = oidutil.appendArgs(return_to, self.return_to_args) + elif immediate: + raise ValueError( + '"return_to" is mandatory when using "checkid_immediate"') + elif self.message.isOpenID1(): + raise ValueError('"return_to" is mandatory for OpenID 1 requests') + elif self.return_to_args: + raise ValueError('extra "return_to" arguments were specified, ' + 'but no return_to was specified') + + if immediate: + mode = 'checkid_immediate' + else: + mode = 'checkid_setup' + + message = self.message.copy() + if message.isOpenID1(): + realm_key = 'trust_root' + else: + realm_key = 'realm' + + message.updateArgs(OPENID_NS, + { + realm_key:realm, + 'mode':mode, + 'return_to':return_to, + }) + + if not self._anonymous: + if self.endpoint.isOPIdentifier(): + # This will never happen when we're in compatibility + # mode, as long as isOPIdentifier() returns False + # whenever preferredNamespace() returns OPENID1_NS. + claimed_id = request_identity = IDENTIFIER_SELECT + else: + request_identity = self.endpoint.getLocalID() + claimed_id = self.endpoint.claimed_id + + # This is true for both OpenID 1 and 2 + message.setArg(OPENID_NS, 'identity', request_identity) + + if message.isOpenID2(): + message.setArg(OPENID2_NS, 'claimed_id', claimed_id) + + if self.assoc: + message.setArg(OPENID_NS, 'assoc_handle', self.assoc.handle) + assoc_log_msg = 'with assocication %s' % (self.assoc.handle,) + else: + assoc_log_msg = 'using stateless mode.' + + oidutil.log("Generated %s request to %s %s" % + (mode, self.endpoint.server_url, assoc_log_msg)) + + return message + + def redirectURL(self, realm, return_to=None, immediate=False): + """Returns a URL with an encoded OpenID request. + + The resulting URL is the OpenID provider's endpoint URL with + parameters appended as query arguments. You should redirect + the user agent to this URL. + + OpenID 2.0 endpoints also accept POST requests, see + C{L{shouldSendRedirect}} and C{L{formMarkup}}. + + @param realm: The URL (or URL pattern) that identifies your + web site to the user when she is authorizing it. + + @type realm: str + + @param return_to: The URL that the OpenID provider will send the + user back to after attempting to verify her identity. + + Not specifying a return_to URL means that the user will not + be returned to the site issuing the request upon its + completion. + + @type return_to: str + + @param immediate: If True, the OpenID provider is to send back + a response immediately, useful for behind-the-scenes + authentication attempts. Otherwise the OpenID provider + may engage the user before providing a response. This is + the default case, as the user may need to provide + credentials or approve the request before a positive + response can be sent. + + @type immediate: bool + + @returns: The URL to redirect the user agent to. + + @returntype: str + """ + message = self.getMessage(realm, return_to, immediate) + return message.toURL(self.endpoint.server_url) + + def formMarkup(self, realm, return_to=None, immediate=False, + form_tag_attrs=None): + """Get html for a form to submit this request to the IDP. + + @param form_tag_attrs: Dictionary of attributes to be added to + the form tag. 'accept-charset' and 'enctype' have defaults + that can be overridden. If a value is supplied for + 'action' or 'method', it will be replaced. + @type form_tag_attrs: {unicode: unicode} + """ + message = self.getMessage(realm, return_to, immediate) + return message.toFormMarkup(self.endpoint.server_url, + form_tag_attrs) + + def htmlMarkup(self, realm, return_to=None, immediate=False, + form_tag_attrs=None): + """Get an autosubmitting HTML page that submits this request to the + IDP. This is just a wrapper for formMarkup. + + @see: formMarkup + + @returns: str + """ + return oidutil.autoSubmitHTML(self.formMarkup(realm, + return_to, + immediate, + form_tag_attrs)) + + def shouldSendRedirect(self): + """Should this OpenID authentication request be sent as a HTTP + redirect or as a POST (form submission)? + + @rtype: bool + """ + return self.endpoint.compatibilityMode() + +FAILURE = 'failure' +SUCCESS = 'success' +CANCEL = 'cancel' +SETUP_NEEDED = 'setup_needed' + +class Response(object): + status = None + + def setEndpoint(self, endpoint): + self.endpoint = endpoint + if endpoint is None: + self.identity_url = None + else: + self.identity_url = endpoint.claimed_id + + def getDisplayIdentifier(self): + """Return the display identifier for this response. + + The display identifier is related to the Claimed Identifier, but the + two are not always identical. The display identifier is something the + user should recognize as what they entered, whereas the response's + claimed identifier (in the L{identity_url} attribute) may have extra + information for better persistence. + + URLs will be stripped of their fragments for display. XRIs will + display the human-readable identifier (i-name) instead of the + persistent identifier (i-number). + + Use the display identifier in your user interface. Use + L{identity_url} for querying your database or authorization server. + """ + if self.endpoint is not None: + return self.endpoint.getDisplayIdentifier() + return None + +class SuccessResponse(Response): + """A response with a status of SUCCESS. Indicates that this request is a + successful acknowledgement from the OpenID server that the + supplied URL is, indeed controlled by the requesting agent. + + @ivar identity_url: The identity URL that has been authenticated; the Claimed Identifier. + See also L{getDisplayIdentifier}. + + @ivar endpoint: The endpoint that authenticated the identifier. You + may access other discovered information related to this endpoint, + such as the CanonicalID of an XRI, through this object. + @type endpoint: L{OpenIDServiceEndpoint} + + @ivar signed_fields: The arguments in the server's response that + were signed and verified. + + @cvar status: SUCCESS + """ + + status = SUCCESS + + def __init__(self, endpoint, message, signed_fields=None): + # Don't use setEndpoint, because endpoint should never be None + # for a successfull transaction. + self.endpoint = endpoint + self.identity_url = endpoint.claimed_id + + self.message = message + + if signed_fields is None: + signed_fields = [] + self.signed_fields = signed_fields + + def isOpenID1(self): + """Was this authentication response an OpenID 1 authentication + response? + """ + return self.message.isOpenID1() + + def isSigned(self, ns_uri, ns_key): + """Return whether a particular key is signed, regardless of + its namespace alias + """ + return self.message.getKey(ns_uri, ns_key) in self.signed_fields + + def getSigned(self, ns_uri, ns_key, default=None): + """Return the specified signed field if available, + otherwise return default + """ + if self.isSigned(ns_uri, ns_key): + return self.message.getArg(ns_uri, ns_key, default) + else: + return default + + def getSignedNS(self, ns_uri): + """Get signed arguments from the response message. Return a + dict of all arguments in the specified namespace. If any of + the arguments are not signed, return None. + """ + msg_args = self.message.getArgs(ns_uri) + + for key in msg_args.iterkeys(): + if not self.isSigned(ns_uri, key): + oidutil.log("SuccessResponse.getSignedNS: (%s, %s) not signed." + % (ns_uri, key)) + return None + + return msg_args + + def extensionResponse(self, namespace_uri, require_signed): + """Return response arguments in the specified namespace. + + @param namespace_uri: The namespace URI of the arguments to be + returned. + + @param require_signed: True if the arguments should be among + those signed in the response, False if you don't care. + + If require_signed is True and the arguments are not signed, + return None. + """ + if require_signed: + return self.getSignedNS(namespace_uri) + else: + return self.message.getArgs(namespace_uri) + + def getReturnTo(self): + """Get the openid.return_to argument from this response. + + This is useful for verifying that this request was initiated + by this consumer. + + @returns: The return_to URL supplied to the server on the + initial request, or C{None} if the response did not contain + an C{openid.return_to} argument. + + @returntype: str + """ + return self.getSigned(OPENID_NS, 'return_to') + + def __eq__(self, other): + return ( + (self.endpoint == other.endpoint) and + (self.identity_url == other.identity_url) and + (self.message == other.message) and + (self.signed_fields == other.signed_fields) and + (self.status == other.status)) + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return '<%s.%s id=%r signed=%r>' % ( + self.__class__.__module__, + self.__class__.__name__, + self.identity_url, self.signed_fields) + + +class FailureResponse(Response): + """A response with a status of FAILURE. Indicates that the OpenID + protocol has failed. This could be locally or remotely triggered. + + @ivar identity_url: The identity URL for which authenitcation was + attempted, if it can be determined. Otherwise, None. + + @ivar message: A message indicating why the request failed, if one + is supplied. otherwise, None. + + @cvar status: FAILURE + """ + + status = FAILURE + + def __init__(self, endpoint, message=None, contact=None, + reference=None): + self.setEndpoint(endpoint) + self.message = message + self.contact = contact + self.reference = reference + + def __repr__(self): + return "<%s.%s id=%r message=%r>" % ( + self.__class__.__module__, self.__class__.__name__, + self.identity_url, self.message) + + +class CancelResponse(Response): + """A response with a status of CANCEL. Indicates that the user + cancelled the OpenID authentication request. + + @ivar identity_url: The identity URL for which authenitcation was + attempted, if it can be determined. Otherwise, None. + + @cvar status: CANCEL + """ + + status = CANCEL + + def __init__(self, endpoint): + self.setEndpoint(endpoint) + +class SetupNeededResponse(Response): + """A response with a status of SETUP_NEEDED. Indicates that the + request was in immediate mode, and the server is unable to + authenticate the user without further interaction. + + @ivar identity_url: The identity URL for which authenitcation was + attempted. + + @ivar setup_url: A URL that can be used to send the user to the + server to set up for authentication. The user should be + redirected in to the setup_url, either in the current window + or in a new browser window. C{None} in OpenID 2.0. + + @cvar status: SETUP_NEEDED + """ + + status = SETUP_NEEDED + + def __init__(self, endpoint, setup_url=None): + self.setEndpoint(endpoint) + self.setup_url = setup_url diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/consumer/discover.py b/desktop/core/ext-py/python-openid-2.2.5/openid/consumer/discover.py new file mode 100644 index 0000000..2c86dda --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/consumer/discover.py @@ -0,0 +1,470 @@ +# -*- test-case-name: openid.test.test_discover -*- +"""Functions to discover OpenID endpoints from identifiers. +""" + +__all__ = [ + 'DiscoveryFailure', + 'OPENID_1_0_NS', + 'OPENID_1_0_TYPE', + 'OPENID_1_1_TYPE', + 'OPENID_2_0_TYPE', + 'OPENID_IDP_2_0_TYPE', + 'OpenIDServiceEndpoint', + 'discover', + ] + +import urlparse + +from openid import oidutil, fetchers, urinorm + +from openid import yadis +from openid.yadis.etxrd import nsTag, XRDSError, XRD_NS_2_0 +from openid.yadis.services import applyFilter as extractServices +from openid.yadis.discover import discover as yadisDiscover +from openid.yadis.discover import DiscoveryFailure +from openid.yadis import xrires, filters +from openid.yadis import xri + +from openid.consumer import html_parse + +OPENID_1_0_NS = 'http://openid.net/xmlns/1.0' +OPENID_IDP_2_0_TYPE = 'http://specs.openid.net/auth/2.0/server' +OPENID_2_0_TYPE = 'http://specs.openid.net/auth/2.0/signon' +OPENID_1_1_TYPE = 'http://openid.net/signon/1.1' +OPENID_1_0_TYPE = 'http://openid.net/signon/1.0' + +from openid.message import OPENID1_NS as OPENID_1_0_MESSAGE_NS +from openid.message import OPENID2_NS as OPENID_2_0_MESSAGE_NS + +class OpenIDServiceEndpoint(object): + """Object representing an OpenID service endpoint. + + @ivar identity_url: the verified identifier. + @ivar canonicalID: For XRI, the persistent identifier. + """ + + # OpenID service type URIs, listed in order of preference. The + # ordering of this list affects yadis and XRI service discovery. + openid_type_uris = [ + OPENID_IDP_2_0_TYPE, + + OPENID_2_0_TYPE, + OPENID_1_1_TYPE, + OPENID_1_0_TYPE, + ] + + def __init__(self): + self.claimed_id = None + self.server_url = None + self.type_uris = [] + self.local_id = None + self.canonicalID = None + self.used_yadis = False # whether this came from an XRDS + self.display_identifier = None + + def usesExtension(self, extension_uri): + return extension_uri in self.type_uris + + def preferredNamespace(self): + if (OPENID_IDP_2_0_TYPE in self.type_uris or + OPENID_2_0_TYPE in self.type_uris): + return OPENID_2_0_MESSAGE_NS + else: + return OPENID_1_0_MESSAGE_NS + + def supportsType(self, type_uri): + """Does this endpoint support this type? + + I consider C{/server} endpoints to implicitly support C{/signon}. + """ + return ( + (type_uri in self.type_uris) or + (type_uri == OPENID_2_0_TYPE and self.isOPIdentifier()) + ) + + def getDisplayIdentifier(self): + """Return the display_identifier if set, else return the claimed_id. + """ + if self.display_identifier is not None: + return self.display_identifier + if self.claimed_id is None: + return None + else: + return urlparse.urldefrag(self.claimed_id)[0] + + def compatibilityMode(self): + return self.preferredNamespace() != OPENID_2_0_MESSAGE_NS + + def isOPIdentifier(self): + return OPENID_IDP_2_0_TYPE in self.type_uris + + def parseService(self, yadis_url, uri, type_uris, service_element): + """Set the state of this object based on the contents of the + service element.""" + self.type_uris = type_uris + self.server_url = uri + self.used_yadis = True + + if not self.isOPIdentifier(): + # XXX: This has crappy implications for Service elements + # that contain both 'server' and 'signon' Types. But + # that's a pathological configuration anyway, so I don't + # think I care. + self.local_id = findOPLocalIdentifier(service_element, + self.type_uris) + self.claimed_id = yadis_url + + def getLocalID(self): + """Return the identifier that should be sent as the + openid.identity parameter to the server.""" + # I looked at this conditional and thought "ah-hah! there's the bug!" + # but Python actually makes that one big expression somehow, i.e. + # "x is x is x" is not the same thing as "(x is x) is x". + # That's pretty weird, dude. -- kmt, 1/07 + if (self.local_id is self.canonicalID is None): + return self.claimed_id + else: + return self.local_id or self.canonicalID + + def fromBasicServiceEndpoint(cls, endpoint): + """Create a new instance of this class from the endpoint + object passed in. + + @return: None or OpenIDServiceEndpoint for this endpoint object""" + type_uris = endpoint.matchTypes(cls.openid_type_uris) + + # If any Type URIs match and there is an endpoint URI + # specified, then this is an OpenID endpoint + if type_uris and endpoint.uri is not None: + openid_endpoint = cls() + openid_endpoint.parseService( + endpoint.yadis_url, + endpoint.uri, + endpoint.type_uris, + endpoint.service_element) + else: + openid_endpoint = None + + return openid_endpoint + + fromBasicServiceEndpoint = classmethod(fromBasicServiceEndpoint) + + def fromHTML(cls, uri, html): + """Parse the given document as HTML looking for an OpenID + + @rtype: [OpenIDServiceEndpoint] + """ + discovery_types = [ + (OPENID_2_0_TYPE, 'openid2.provider', 'openid2.local_id'), + (OPENID_1_1_TYPE, 'openid.server', 'openid.delegate'), + ] + + link_attrs = html_parse.parseLinkAttrs(html) + services = [] + for type_uri, op_endpoint_rel, local_id_rel in discovery_types: + op_endpoint_url = html_parse.findFirstHref( + link_attrs, op_endpoint_rel) + if op_endpoint_url is None: + continue + + service = cls() + service.claimed_id = uri + service.local_id = html_parse.findFirstHref( + link_attrs, local_id_rel) + service.server_url = op_endpoint_url + service.type_uris = [type_uri] + + services.append(service) + + return services + + fromHTML = classmethod(fromHTML) + + + def fromXRDS(cls, uri, xrds): + """Parse the given document as XRDS looking for OpenID services. + + @rtype: [OpenIDServiceEndpoint] + + @raises XRDSError: When the XRDS does not parse. + + @since: 2.1.0 + """ + return extractServices(uri, xrds, cls) + + fromXRDS = classmethod(fromXRDS) + + + def fromDiscoveryResult(cls, discoveryResult): + """Create endpoints from a DiscoveryResult. + + @type discoveryResult: L{DiscoveryResult} + + @rtype: list of L{OpenIDServiceEndpoint} + + @raises XRDSError: When the XRDS does not parse. + + @since: 2.1.0 + """ + if discoveryResult.isXRDS(): + method = cls.fromXRDS + else: + method = cls.fromHTML + return method(discoveryResult.normalized_uri, + discoveryResult.response_text) + + fromDiscoveryResult = classmethod(fromDiscoveryResult) + + + def fromOPEndpointURL(cls, op_endpoint_url): + """Construct an OP-Identifier OpenIDServiceEndpoint object for + a given OP Endpoint URL + + @param op_endpoint_url: The URL of the endpoint + @rtype: OpenIDServiceEndpoint + """ + service = cls() + service.server_url = op_endpoint_url + service.type_uris = [OPENID_IDP_2_0_TYPE] + return service + + fromOPEndpointURL = classmethod(fromOPEndpointURL) + + + def __str__(self): + return ("<%s.%s " + "server_url=%r " + "claimed_id=%r " + "local_id=%r " + "canonicalID=%r " + "used_yadis=%s " + ">" + % (self.__class__.__module__, self.__class__.__name__, + self.server_url, + self.claimed_id, + self.local_id, + self.canonicalID, + self.used_yadis)) + + + +def findOPLocalIdentifier(service_element, type_uris): + """Find the OP-Local Identifier for this xrd:Service element. + + This considers openid:Delegate to be a synonym for xrd:LocalID if + both OpenID 1.X and OpenID 2.0 types are present. If only OpenID + 1.X is present, it returns the value of openid:Delegate. If only + OpenID 2.0 is present, it returns the value of xrd:LocalID. If + there is more than one LocalID tag and the values are different, + it raises a DiscoveryFailure. This is also triggered when the + xrd:LocalID and openid:Delegate tags are different. + + @param service_element: The xrd:Service element + @type service_element: ElementTree.Node + + @param type_uris: The xrd:Type values present in this service + element. This function could extract them, but higher level + code needs to do that anyway. + @type type_uris: [str] + + @raises DiscoveryFailure: when discovery fails. + + @returns: The OP-Local Identifier for this service element, if one + is present, or None otherwise. + @rtype: str or unicode or NoneType + """ + # XXX: Test this function on its own! + + # Build the list of tags that could contain the OP-Local Identifier + local_id_tags = [] + if (OPENID_1_1_TYPE in type_uris or + OPENID_1_0_TYPE in type_uris): + local_id_tags.append(nsTag(OPENID_1_0_NS, 'Delegate')) + + if OPENID_2_0_TYPE in type_uris: + local_id_tags.append(nsTag(XRD_NS_2_0, 'LocalID')) + + # Walk through all the matching tags and make sure that they all + # have the same value + local_id = None + for local_id_tag in local_id_tags: + for local_id_element in service_element.findall(local_id_tag): + if local_id is None: + local_id = local_id_element.text + elif local_id != local_id_element.text: + format = 'More than one %r tag found in one service element' + message = format % (local_id_tag,) + raise DiscoveryFailure(message, None) + + return local_id + +def normalizeURL(url): + """Normalize a URL, converting normalization failures to + DiscoveryFailure""" + try: + normalized = urinorm.urinorm(url) + except ValueError, why: + raise DiscoveryFailure('Normalizing identifier: %s' % (why[0],), None) + else: + return urlparse.urldefrag(normalized)[0] + +def normalizeXRI(xri): + """Normalize an XRI, stripping its scheme if present""" + if xri.startswith("xri://"): + xri = xri[6:] + return xri + +def arrangeByType(service_list, preferred_types): + """Rearrange service_list in a new list so services are ordered by + types listed in preferred_types. Return the new list.""" + + def enumerate(elts): + """Return an iterable that pairs the index of an element with + that element. + + For Python 2.2 compatibility""" + return zip(range(len(elts)), elts) + + def bestMatchingService(service): + """Return the index of the first matching type, or something + higher if no type matches. + + This provides an ordering in which service elements that + contain a type that comes earlier in the preferred types list + come before service elements that come later. If a service + element has more than one type, the most preferred one wins. + """ + for i, t in enumerate(preferred_types): + if preferred_types[i] in service.type_uris: + return i + + return len(preferred_types) + + # Build a list with the service elements in tuples whose + # comparison will prefer the one with the best matching service + prio_services = [(bestMatchingService(s), orig_index, s) + for (orig_index, s) in enumerate(service_list)] + prio_services.sort() + + # Now that the services are sorted by priority, remove the sort + # keys from the list. + for i in range(len(prio_services)): + prio_services[i] = prio_services[i][2] + + return prio_services + +def getOPOrUserServices(openid_services): + """Extract OP Identifier services. If none found, return the + rest, sorted with most preferred first according to + OpenIDServiceEndpoint.openid_type_uris. + + openid_services is a list of OpenIDServiceEndpoint objects. + + Returns a list of OpenIDServiceEndpoint objects.""" + + op_services = arrangeByType(openid_services, [OPENID_IDP_2_0_TYPE]) + + openid_services = arrangeByType(openid_services, + OpenIDServiceEndpoint.openid_type_uris) + + return op_services or openid_services + +def discoverYadis(uri): + """Discover OpenID services for a URI. Tries Yadis and falls back + on old-style discovery if Yadis fails. + + @param uri: normalized identity URL + @type uri: str + + @return: (claimed_id, services) + @rtype: (str, list(OpenIDServiceEndpoint)) + + @raises DiscoveryFailure: when discovery fails. + """ + # Might raise a yadis.discover.DiscoveryFailure if no document + # came back for that URI at all. I don't think falling back + # to OpenID 1.0 discovery on the same URL will help, so don't + # bother to catch it. + response = yadisDiscover(uri) + + yadis_url = response.normalized_uri + body = response.response_text + try: + openid_services = OpenIDServiceEndpoint.fromXRDS(yadis_url, body) + except XRDSError: + # Does not parse as a Yadis XRDS file + openid_services = [] + + if not openid_services: + # Either not an XRDS or there are no OpenID services. + + if response.isXRDS(): + # if we got the Yadis content-type or followed the Yadis + # header, re-fetch the document without following the Yadis + # header, with no Accept header. + return discoverNoYadis(uri) + + # Try to parse the response as HTML. + # + openid_services = OpenIDServiceEndpoint.fromHTML(yadis_url, body) + + return (yadis_url, getOPOrUserServices(openid_services)) + +def discoverXRI(iname): + endpoints = [] + iname = normalizeXRI(iname) + try: + canonicalID, services = xrires.ProxyResolver().query( + iname, OpenIDServiceEndpoint.openid_type_uris) + + if canonicalID is None: + raise XRDSError('No CanonicalID found for XRI %r' % (iname,)) + + flt = filters.mkFilter(OpenIDServiceEndpoint) + for service_element in services: + endpoints.extend(flt.getServiceEndpoints(iname, service_element)) + except XRDSError: + oidutil.log('xrds error on ' + iname) + + for endpoint in endpoints: + # Is there a way to pass this through the filter to the endpoint + # constructor instead of tacking it on after? + endpoint.canonicalID = canonicalID + endpoint.claimed_id = canonicalID + endpoint.display_identifier = iname + + # FIXME: returned xri should probably be in some normal form + return iname, getOPOrUserServices(endpoints) + + +def discoverNoYadis(uri): + http_resp = fetchers.fetch(uri) + if http_resp.status not in (200, 206): + raise DiscoveryFailure( + 'HTTP Response status from identity URL host is not 200. ' + 'Got status %r' % (http_resp.status,), http_resp) + + claimed_id = http_resp.final_url + openid_services = OpenIDServiceEndpoint.fromHTML( + claimed_id, http_resp.body) + return claimed_id, openid_services + +def discoverURI(uri): + parsed = urlparse.urlparse(uri) + if parsed[0] and parsed[1]: + if parsed[0] not in ['http', 'https']: + raise DiscoveryFailure('URI scheme is not HTTP or HTTPS', None) + else: + uri = 'http://' + uri + + uri = normalizeURL(uri) + claimed_id, openid_services = discoverYadis(uri) + claimed_id = normalizeURL(claimed_id) + return claimed_id, openid_services + +def discover(identifier): + if xri.identifierScheme(identifier) == "XRI": + return discoverXRI(identifier) + else: + return discoverURI(identifier) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/consumer/html_parse.py b/desktop/core/ext-py/python-openid-2.2.5/openid/consumer/html_parse.py new file mode 100644 index 0000000..880dfda --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/consumer/html_parse.py @@ -0,0 +1,249 @@ +""" +This module implements a VERY limited parser that finds tags in +the head of HTML or XHTML documents and parses out their attributes +according to the OpenID spec. It is a liberal parser, but it requires +these things from the data in order to work: + + - There must be an open tag + + - There must be an open tag inside of the tag + + - Only s that are found inside of the tag are parsed + (this is by design) + + - The parser follows the OpenID specification in resolving the + attributes of the link tags. This means that the attributes DO NOT + get resolved as they would by an XML or HTML parser. In particular, + only certain entities get replaced, and href attributes do not get + resolved relative to a base URL. + +From http://openid.net/specs.bml#linkrel: + + - The openid.server URL MUST be an absolute URL. OpenID consumers + MUST NOT attempt to resolve relative URLs. + + - The openid.server URL MUST NOT include entities other than &, + <, >, and ". + +The parser ignores SGML comments and . Both kinds of +quoting are allowed for attributes. + +The parser deals with invalid markup in these ways: + + - Tag names are not case-sensitive + + - The tag is accepted even when it is not at the top level + + - The tag is accepted even when it is not a direct child of + the tag, but a tag must be an ancestor of the + tag + + - tags are accepted even when they are not direct children of + the tag, but a tag must be an ancestor of the + tag + + - If there is no closing tag for an open or tag, the + remainder of the document is viewed as being inside of the tag. If + there is no closing tag for a tag, the link tag is treated + as a short tag. Exceptions to this rule are that closes + and or closes + + - Attributes of the tag are not required to be quoted. + + - In the case of duplicated attribute names, the attribute coming + last in the tag will be the value returned. + + - Any text that does not parse as an attribute within a link tag will + be ignored. (e.g. will ignore + pumpkin) + + - If there are more than one or tag, the parser only + looks inside of the first one. + + - The contents of + +''', flags) + +tag_expr = r''' +# Starts with the tag name at a word boundary, where the tag name is +# not a namespace +<%(tag_name)s\b(?!:) + +# All of the stuff up to a ">", hopefully attributes. +(?P[^>]*?) + +(?: # Match a short tag + /> + +| # Match a full tag + > + + (?P.*?) + + # Closed by + (?: # One of the specified close tags + + + # End of the string + | \Z + + ) + +) +''' + +def tagMatcher(tag_name, *close_tags): + if close_tags: + options = '|'.join((tag_name,) + close_tags) + closers = '(?:%s)' % (options,) + else: + closers = tag_name + + expr = tag_expr % locals() + return re.compile(expr, flags) + +# Must contain at least an open html and an open head tag +html_find = tagMatcher('html') +head_find = tagMatcher('head', 'body') +link_find = re.compile(r'\w+)= + +# Then either a quoted or unquoted attribute +(?: + + # Match everything that\'s between matching quote marks + (?P["\'])(?P.*?)(?P=qopen) +| + + # If the value is not quoted, match up to whitespace + (?P(?:[^\s<>/]|/(?!>))+) +) + +| + +(?P[<>]) +''', flags) + +# Entity replacement: +replacements = { + 'amp':'&', + 'lt':'<', + 'gt':'>', + 'quot':'"', + } + +ent_replace = re.compile(r'&(%s);' % '|'.join(replacements.keys())) +def replaceEnt(mo): + "Replace the entities that are specified by OpenID" + return replacements.get(mo.group(1), mo.group()) + +def parseLinkAttrs(html): + """Find all link tags in a string representing a HTML document and + return a list of their attributes. + + @param html: the text to parse + @type html: str or unicode + + @return: A list of dictionaries of attributes, one for each link tag + @rtype: [[(type(html), type(html))]] + """ + stripped = removed_re.sub('', html) + html_mo = html_find.search(stripped) + if html_mo is None or html_mo.start('contents') == -1: + return [] + + start, end = html_mo.span('contents') + head_mo = head_find.search(stripped, start, end) + if head_mo is None or head_mo.start('contents') == -1: + return [] + + start, end = head_mo.span('contents') + link_mos = link_find.finditer(stripped, head_mo.start(), head_mo.end()) + + matches = [] + for link_mo in link_mos: + start = link_mo.start() + 5 + link_attrs = {} + for attr_mo in attr_find.finditer(stripped, start): + if attr_mo.lastgroup == 'end_link': + break + + # Either q_val or unq_val must be present, but not both + # unq_val is a True (non-empty) value if it is present + attr_name, q_val, unq_val = attr_mo.group( + 'attr_name', 'q_val', 'unq_val') + attr_val = ent_replace.sub(replaceEnt, unq_val or q_val) + + link_attrs[attr_name] = attr_val + + matches.append(link_attrs) + + return matches + +def relMatches(rel_attr, target_rel): + """Does this target_rel appear in the rel_str?""" + # XXX: TESTME + rels = rel_attr.strip().split() + for rel in rels: + rel = rel.lower() + if rel == target_rel: + return 1 + + return 0 + +def linkHasRel(link_attrs, target_rel): + """Does this link have target_rel as a relationship?""" + # XXX: TESTME + rel_attr = link_attrs.get('rel') + return rel_attr and relMatches(rel_attr, target_rel) + +def findLinksRel(link_attrs_list, target_rel): + """Filter the list of link attributes on whether it has target_rel + as a relationship.""" + # XXX: TESTME + matchesTarget = lambda attrs: linkHasRel(attrs, target_rel) + return filter(matchesTarget, link_attrs_list) + +def findFirstHref(link_attrs_list, target_rel): + """Return the value of the href attribute for the first link tag + in the list that has target_rel as a relationship.""" + # XXX: TESTME + matches = findLinksRel(link_attrs_list, target_rel) + if not matches: + return None + first = matches[0] + return first.get('href') diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/cryptutil.py b/desktop/core/ext-py/python-openid-2.2.5/openid/cryptutil.py new file mode 100644 index 0000000..3123be6 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/cryptutil.py @@ -0,0 +1,220 @@ +"""Module containing a cryptographic-quality source of randomness and +other cryptographically useful functionality + +Python 2.4 needs no external support for this module, nor does Python +2.3 on a system with /dev/urandom. + +Other configurations will need a quality source of random bytes and +access to a function that will convert binary strings to long +integers. This module will work with the Python Cryptography Toolkit +(pycrypto) if it is present. pycrypto can be found with a search +engine, but is currently found at: + +http://www.amk.ca/python/code/crypto +""" + +__all__ = [ + 'base64ToLong', + 'binaryToLong', + 'hmacSha1', + 'hmacSha256', + 'longToBase64', + 'longToBinary', + 'randomString', + 'randrange', + 'sha1', + 'sha256', + ] + +import hmac +import os +import random + +from openid.oidutil import toBase64, fromBase64 + +try: + import hashlib +except ImportError: + import sha as sha1_module + + try: + from Crypto.Hash import SHA256 as sha256_module + except ImportError: + sha256_module = None + +else: + class HashContainer(object): + def __init__(self, hash_constructor): + self.new = hash_constructor + self.digest_size = hash_constructor().digest_size + + sha1_module = HashContainer(hashlib.sha1) + sha256_module = HashContainer(hashlib.sha256) + +def hmacSha1(key, text): + return hmac.new(key, text, sha1_module).digest() + +def sha1(s): + return sha1_module.new(s).digest() + +if sha256_module is not None: + def hmacSha256(key, text): + return hmac.new(key, text, sha256_module).digest() + + def sha256(s): + return sha256_module.new(s).digest() + + SHA256_AVAILABLE = True + +else: + _no_sha256 = NotImplementedError( + 'Use Python 2.5, install pycrypto or install hashlib to use SHA256') + + def hmacSha256(unused_key, unused_text): + raise _no_sha256 + + def sha256(s): + raise _no_sha256 + + SHA256_AVAILABLE = False + +try: + from Crypto.Util.number import long_to_bytes, bytes_to_long +except ImportError: + import pickle + try: + # Check Python compatiblity by raising an exception on import + # if the needed functionality is not present. Present in + # Python >= 2.3 + pickle.encode_long + pickle.decode_long + except AttributeError: + raise ImportError( + 'No functionality for serializing long integers found') + + # Present in Python >= 2.4 + try: + reversed + except NameError: + def reversed(seq): + return map(seq.__getitem__, xrange(len(seq) - 1, -1, -1)) + + def longToBinary(l): + if l == 0: + return '\x00' + + return ''.join(reversed(pickle.encode_long(l))) + + def binaryToLong(s): + return pickle.decode_long(''.join(reversed(s))) +else: + # We have pycrypto + + def longToBinary(l): + if l < 0: + raise ValueError('This function only supports positive integers') + + bytes = long_to_bytes(l) + if ord(bytes[0]) > 127: + return '\x00' + bytes + else: + return bytes + + def binaryToLong(bytes): + if not bytes: + raise ValueError('Empty string passed to strToLong') + + if ord(bytes[0]) > 127: + raise ValueError('This function only supports positive integers') + + return bytes_to_long(bytes) + +# A cryptographically safe source of random bytes +try: + getBytes = os.urandom +except AttributeError: + try: + from Crypto.Util.randpool import RandomPool + except ImportError: + # Fall back on /dev/urandom, if present. It would be nice to + # have Windows equivalent here, but for now, require pycrypto + # on Windows. + try: + _urandom = file('/dev/urandom', 'rb') + except IOError: + raise ImportError('No adequate source of randomness found!') + else: + def getBytes(n): + bytes = [] + while n: + chunk = _urandom.read(n) + n -= len(chunk) + bytes.append(chunk) + assert n >= 0 + return ''.join(bytes) + else: + _pool = RandomPool() + def getBytes(n, pool=_pool): + if pool.entropy < n: + pool.randomize() + return pool.get_bytes(n) + +# A randrange function that works for longs +try: + randrange = random.SystemRandom().randrange +except AttributeError: + # In Python 2.2's random.Random, randrange does not support + # numbers larger than sys.maxint for randrange. For simplicity, + # use this implementation for any Python that does not have + # random.SystemRandom + from math import log, ceil + + _duplicate_cache = {} + def randrange(start, stop=None, step=1): + if stop is None: + stop = start + start = 0 + + r = (stop - start) // step + try: + (duplicate, nbytes) = _duplicate_cache[r] + except KeyError: + rbytes = longToBinary(r) + if rbytes[0] == '\x00': + nbytes = len(rbytes) - 1 + else: + nbytes = len(rbytes) + + mxrand = (256 ** nbytes) + + # If we get a number less than this, then it is in the + # duplicated range. + duplicate = mxrand % r + + if len(_duplicate_cache) > 10: + _duplicate_cache.clear() + + _duplicate_cache[r] = (duplicate, nbytes) + + while 1: + bytes = '\x00' + getBytes(nbytes) + n = binaryToLong(bytes) + # Keep looping if this value is in the low duplicated range + if n >= duplicate: + break + + return start + (n % r) * step + +def longToBase64(l): + return toBase64(longToBinary(l)) + +def base64ToLong(s): + return binaryToLong(fromBase64(s)) + +def randomString(length, chrs=None): + """Produce a string of length random bytes, chosen from chrs.""" + if chrs is None: + return getBytes(length) + else: + n = len(chrs) + return ''.join([chrs[randrange(n)] for _ in xrange(length)]) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/dh.py b/desktop/core/ext-py/python-openid-2.2.5/openid/dh.py new file mode 100644 index 0000000..bb83bbe --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/dh.py @@ -0,0 +1,42 @@ +from openid import cryptutil +from openid import oidutil + +def strxor(x, y): + if len(x) != len(y): + raise ValueError('Inputs to strxor must have the same length') + + xor = lambda (a, b): chr(ord(a) ^ ord(b)) + return "".join(map(xor, zip(x, y))) + +class DiffieHellman(object): + DEFAULT_MOD = 155172898181473697471232257763715539915724801966915404479707795314057629378541917580651227423698188993727816152646631438561595825688188889951272158842675419950341258706556549803580104870537681476726513255747040765857479291291572334510643245094715007229621094194349783925984760375594985848253359305585439638443L + + DEFAULT_GEN = 2 + + def fromDefaults(cls): + return cls(cls.DEFAULT_MOD, cls.DEFAULT_GEN) + + fromDefaults = classmethod(fromDefaults) + + def __init__(self, modulus, generator): + self.modulus = long(modulus) + self.generator = long(generator) + + self._setPrivate(cryptutil.randrange(1, modulus - 1)) + + def _setPrivate(self, private): + """This is here to make testing easier""" + self.private = private + self.public = pow(self.generator, self.private, self.modulus) + + def usingDefaultValues(self): + return (self.modulus == self.DEFAULT_MOD and + self.generator == self.DEFAULT_GEN) + + def getSharedSecret(self, composite): + return pow(composite, self.private, self.modulus) + + def xorSecret(self, composite, secret, hash_func): + dh_shared = self.getSharedSecret(composite) + hashed_dh_shared = hash_func(cryptutil.longToBinary(dh_shared)) + return strxor(secret, hashed_dh_shared) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/extension.py b/desktop/core/ext-py/python-openid-2.2.5/openid/extension.py new file mode 100644 index 0000000..d48bbb2 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/extension.py @@ -0,0 +1,46 @@ +from openid import message as message_module + +class Extension(object): + """An interface for OpenID extensions. + + @ivar ns_uri: The namespace to which to add the arguments for this + extension + """ + ns_uri = None + ns_alias = None + + def getExtensionArgs(self): + """Get the string arguments that should be added to an OpenID + message for this extension. + + @returns: A dictionary of completely non-namespaced arguments + to be added. For example, if the extension's alias is + 'uncle', and this method returns {'meat':'Hot Rats'}, the + final message will contain {'openid.uncle.meat':'Hot Rats'} + """ + raise NotImplementedError + + def toMessage(self, message=None): + """Add the arguments from this extension to the provided + message, or create a new message containing only those + arguments. + + @returns: The message with the extension arguments added + """ + if message is None: + warnings.warn('Passing None to Extension.toMessage is deprecated. ' + 'Creating a message assuming you want OpenID 2.', + DeprecationWarning, stacklevel=2) + message = message_module.Message(message_module.OPENID2_NS) + + implicit = message.isOpenID1() + + try: + message.namespaces.addAlias(self.ns_uri, self.ns_alias, + implicit=implicit) + except KeyError: + if message.namespaces.getAlias(self.ns_uri) != self.ns_alias: + raise + + message.updateArgs(self.ns_uri, self.getExtensionArgs()) + return message diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/extensions/__init__.py b/desktop/core/ext-py/python-openid-2.2.5/openid/extensions/__init__.py new file mode 100644 index 0000000..710b200 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/extensions/__init__.py @@ -0,0 +1,5 @@ +"""OpenID Extension modules.""" + +__all__ = ['ax', 'pape', 'sreg'] + +from openid.extensions.draft import pape5 as pape diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/extensions/ax.py b/desktop/core/ext-py/python-openid-2.2.5/openid/extensions/ax.py new file mode 100644 index 0000000..65d0a51 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/extensions/ax.py @@ -0,0 +1,774 @@ +# -*- test-case-name: openid.test.test_ax -*- +"""Implements the OpenID Attribute Exchange specification, version 1.0. + +@since: 2.1.0 +""" + +__all__ = [ + 'AttributeRequest', + 'FetchRequest', + 'FetchResponse', + 'StoreRequest', + 'StoreResponse', + ] + +from openid import extension +from openid.server.trustroot import TrustRoot +from openid.message import NamespaceMap, OPENID_NS + +# Use this as the 'count' value for an attribute in a FetchRequest to +# ask for as many values as the OP can provide. +UNLIMITED_VALUES = "unlimited" + +# Minimum supported alias length in characters. Here for +# completeness. +MINIMUM_SUPPORTED_ALIAS_LENGTH = 32 + +def checkAlias(alias): + """ + Check an alias for invalid characters; raise AXError if any are + found. Return None if the alias is valid. + """ + if ',' in alias: + raise AXError("Alias %r must not contain comma" % (alias,)) + if '.' in alias: + raise AXError("Alias %r must not contain period" % (alias,)) + + +class AXError(ValueError): + """Results from data that does not meet the attribute exchange 1.0 + specification""" + + +class NotAXMessage(AXError): + """Raised when there is no Attribute Exchange mode in the message.""" + + def __repr__(self): + return self.__class__.__name__ + + def __str__(self): + return self.__class__.__name__ + + +class AXMessage(extension.Extension): + """Abstract class containing common code for attribute exchange messages + + @cvar ns_alias: The preferred namespace alias for attribute + exchange messages + + @cvar mode: The type of this attribute exchange message. This must + be overridden in subclasses. + """ + + # This class is abstract, so it's OK that it doesn't override the + # abstract method in Extension: + # + #pylint:disable-msg=W0223 + + ns_alias = 'ax' + mode = None + ns_uri = 'http://openid.net/srv/ax/1.0' + + def _checkMode(self, ax_args): + """Raise an exception if the mode in the attribute exchange + arguments does not match what is expected for this class. + + @raises NotAXMessage: When there is no mode value in ax_args at all. + + @raises AXError: When mode does not match. + """ + mode = ax_args.get('mode') + if mode != self.mode: + if not mode: + raise NotAXMessage() + else: + raise AXError( + 'Expected mode %r; got %r' % (self.mode, mode)) + + def _newArgs(self): + """Return a set of attribute exchange arguments containing the + basic information that must be in every attribute exchange + message. + """ + return {'mode':self.mode} + + +class AttrInfo(object): + """Represents a single attribute in an attribute exchange + request. This should be added to an AXRequest object in order to + request the attribute. + + @ivar required: Whether the attribute will be marked as required + when presented to the subject of the attribute exchange + request. + @type required: bool + + @ivar count: How many values of this type to request from the + subject. Defaults to one. + @type count: int + + @ivar type_uri: The identifier that determines what the attribute + represents and how it is serialized. For example, one type URI + representing dates could represent a Unix timestamp in base 10 + and another could represent a human-readable string. + @type type_uri: str + + @ivar alias: The name that should be given to this alias in the + request. If it is not supplied, a generic name will be + assigned. For example, if you want to call a Unix timestamp + value 'tstamp', set its alias to that value. If two attributes + in the same message request to use the same alias, the request + will fail to be generated. + @type alias: str or NoneType + """ + + # It's OK that this class doesn't have public methods (it's just a + # holder for a bunch of attributes): + # + #pylint:disable-msg=R0903 + + def __init__(self, type_uri, count=1, required=False, alias=None): + self.required = required + self.count = count + self.type_uri = type_uri + self.alias = alias + + if self.alias is not None: + checkAlias(self.alias) + + def wantsUnlimitedValues(self): + """ + When processing a request for this attribute, the OP should + call this method to determine whether all available attribute + values were requested. If self.count == UNLIMITED_VALUES, + this returns True. Otherwise this returns False, in which + case self.count is an integer. + """ + return self.count == UNLIMITED_VALUES + +def toTypeURIs(namespace_map, alias_list_s): + """Given a namespace mapping and a string containing a + comma-separated list of namespace aliases, return a list of type + URIs that correspond to those aliases. + + @param namespace_map: The mapping from namespace URI to alias + @type namespace_map: openid.message.NamespaceMap + + @param alias_list_s: The string containing the comma-separated + list of aliases. May also be None for convenience. + @type alias_list_s: str or NoneType + + @returns: The list of namespace URIs that corresponds to the + supplied list of aliases. If the string was zero-length or + None, an empty list will be returned. + + @raise KeyError: If an alias is present in the list of aliases but + is not present in the namespace map. + """ + uris = [] + + if alias_list_s: + for alias in alias_list_s.split(','): + type_uri = namespace_map.getNamespaceURI(alias) + if type_uri is None: + raise KeyError( + 'No type is defined for attribute name %r' % (alias,)) + else: + uris.append(type_uri) + + return uris + + +class FetchRequest(AXMessage): + """An attribute exchange 'fetch_request' message. This message is + sent by a relying party when it wishes to obtain attributes about + the subject of an OpenID authentication request. + + @ivar requested_attributes: The attributes that have been + requested thus far, indexed by the type URI. + @type requested_attributes: {str:AttrInfo} + + @ivar update_url: A URL that will accept responses for this + attribute exchange request, even in the absence of the user + who made this request. + """ + mode = 'fetch_request' + + def __init__(self, update_url=None): + AXMessage.__init__(self) + self.requested_attributes = {} + self.update_url = update_url + + def add(self, attribute): + """Add an attribute to this attribute exchange request. + + @param attribute: The attribute that is being requested + @type attribute: C{L{AttrInfo}} + + @returns: None + + @raise KeyError: when the requested attribute is already + present in this fetch request. + """ + if attribute.type_uri in self.requested_attributes: + raise KeyError('The attribute %r has already been requested' + % (attribute.type_uri,)) + + self.requested_attributes[attribute.type_uri] = attribute + + def getExtensionArgs(self): + """Get the serialized form of this attribute fetch request. + + @returns: The fetch request message parameters + @rtype: {unicode:unicode} + """ + aliases = NamespaceMap() + + required = [] + if_available = [] + + ax_args = self._newArgs() + + for type_uri, attribute in self.requested_attributes.iteritems(): + if attribute.alias is None: + alias = aliases.add(type_uri) + else: + # This will raise an exception when the second + # attribute with the same alias is added. I think it + # would be better to complain at the time that the + # attribute is added to this object so that the code + # that is adding it is identified in the stack trace, + # but it's more work to do so, and it won't be 100% + # accurate anyway, since the attributes are + # mutable. So for now, just live with the fact that + # we'll learn about the error later. + # + # The other possible approach is to hide the error and + # generate a new alias on the fly. I think that would + # probably be bad. + alias = aliases.addAlias(type_uri, attribute.alias) + + if attribute.required: + required.append(alias) + else: + if_available.append(alias) + + if attribute.count != 1: + ax_args['count.' + alias] = str(attribute.count) + + ax_args['type.' + alias] = type_uri + + if required: + ax_args['required'] = ','.join(required) + + if if_available: + ax_args['if_available'] = ','.join(if_available) + + return ax_args + + def getRequiredAttrs(self): + """Get the type URIs for all attributes that have been marked + as required. + + @returns: A list of the type URIs for attributes that have + been marked as required. + @rtype: [str] + """ + required = [] + for type_uri, attribute in self.requested_attributes.iteritems(): + if attribute.required: + required.append(type_uri) + + return required + + def fromOpenIDRequest(cls, openid_request): + """Extract a FetchRequest from an OpenID message + + @param openid_request: The OpenID authentication request + containing the attribute fetch request + @type openid_request: C{L{openid.server.server.CheckIDRequest}} + + @rtype: C{L{FetchRequest}} or C{None} + @returns: The FetchRequest extracted from the message or None, if + the message contained no AX extension. + + @raises KeyError: if the AuthRequest is not consistent in its use + of namespace aliases. + + @raises AXError: When parseExtensionArgs would raise same. + + @see: L{parseExtensionArgs} + """ + message = openid_request.message + ax_args = message.getArgs(cls.ns_uri) + self = cls() + try: + self.parseExtensionArgs(ax_args) + except NotAXMessage, err: + return None + + if self.update_url: + # Update URL must match the openid.realm of the underlying + # OpenID 2 message. + realm = message.getArg(OPENID_NS, 'realm', + message.getArg(OPENID_NS, 'return_to')) + + if not realm: + raise AXError(("Cannot validate update_url %r " + + "against absent realm") % (self.update_url,)) + + tr = TrustRoot.parse(realm) + if not tr.validateURL(self.update_url): + raise AXError("Update URL %r failed validation against realm %r" % + (self.update_url, realm,)) + + return self + + fromOpenIDRequest = classmethod(fromOpenIDRequest) + + def parseExtensionArgs(self, ax_args): + """Given attribute exchange arguments, populate this FetchRequest. + + @param ax_args: Attribute Exchange arguments from the request. + As returned from L{Message.getArgs}. + @type ax_args: dict + + @raises KeyError: if the message is not consistent in its use + of namespace aliases. + + @raises NotAXMessage: If ax_args does not include an Attribute Exchange + mode. + + @raises AXError: If the data to be parsed does not follow the + attribute exchange specification. At least when + 'if_available' or 'required' is not specified for a + particular attribute type. + """ + # Raises an exception if the mode is not the expected value + self._checkMode(ax_args) + + aliases = NamespaceMap() + + for key, value in ax_args.iteritems(): + if key.startswith('type.'): + alias = key[5:] + type_uri = value + aliases.addAlias(type_uri, alias) + + count_key = 'count.' + alias + count_s = ax_args.get(count_key) + if count_s: + try: + count = int(count_s) + if count <= 0: + raise AXError("Count %r must be greater than zero, got %r" % (count_key, count_s,)) + except ValueError: + if count_s != UNLIMITED_VALUES: + raise AXError("Invalid count value for %r: %r" % (count_key, count_s,)) + count = count_s + else: + count = 1 + + self.add(AttrInfo(type_uri, alias=alias, count=count)) + + required = toTypeURIs(aliases, ax_args.get('required')) + + for type_uri in required: + self.requested_attributes[type_uri].required = True + + if_available = toTypeURIs(aliases, ax_args.get('if_available')) + + all_type_uris = required + if_available + + for type_uri in aliases.iterNamespaceURIs(): + if type_uri not in all_type_uris: + raise AXError( + 'Type URI %r was in the request but not ' + 'present in "required" or "if_available"' % (type_uri,)) + + self.update_url = ax_args.get('update_url') + + def iterAttrs(self): + """Iterate over the AttrInfo objects that are + contained in this fetch_request. + """ + return self.requested_attributes.itervalues() + + def __iter__(self): + """Iterate over the attribute type URIs in this fetch_request + """ + return iter(self.requested_attributes) + + def has_key(self, type_uri): + """Is the given type URI present in this fetch_request? + """ + return type_uri in self.requested_attributes + + __contains__ = has_key + + +class AXKeyValueMessage(AXMessage): + """An abstract class that implements a message that has attribute + keys and values. It contains the common code between + fetch_response and store_request. + """ + + # This class is abstract, so it's OK that it doesn't override the + # abstract method in Extension: + # + #pylint:disable-msg=W0223 + + def __init__(self): + AXMessage.__init__(self) + self.data = {} + + def addValue(self, type_uri, value): + """Add a single value for the given attribute type to the + message. If there are already values specified for this type, + this value will be sent in addition to the values already + specified. + + @param type_uri: The URI for the attribute + + @param value: The value to add to the response to the relying + party for this attribute + @type value: unicode + + @returns: None + """ + try: + values = self.data[type_uri] + except KeyError: + values = self.data[type_uri] = [] + + values.append(value) + + def setValues(self, type_uri, values): + """Set the values for the given attribute type. This replaces + any values that have already been set for this attribute. + + @param type_uri: The URI for the attribute + + @param values: A list of values to send for this attribute. + @type values: [unicode] + """ + + self.data[type_uri] = values + + def _getExtensionKVArgs(self, aliases=None): + """Get the extension arguments for the key/value pairs + contained in this message. + + @param aliases: An alias mapping. Set to None if you don't + care about the aliases for this request. + """ + if aliases is None: + aliases = NamespaceMap() + + ax_args = {} + + for type_uri, values in self.data.iteritems(): + alias = aliases.add(type_uri) + + ax_args['type.' + alias] = type_uri + ax_args['count.' + alias] = str(len(values)) + + for i, value in enumerate(values): + key = 'value.%s.%d' % (alias, i + 1) + ax_args[key] = value + + return ax_args + + def parseExtensionArgs(self, ax_args): + """Parse attribute exchange key/value arguments into this + object. + + @param ax_args: The attribute exchange fetch_response + arguments, with namespacing removed. + @type ax_args: {unicode:unicode} + + @returns: None + + @raises ValueError: If the message has bad values for + particular fields + + @raises KeyError: If the namespace mapping is bad or required + arguments are missing + """ + self._checkMode(ax_args) + + aliases = NamespaceMap() + + for key, value in ax_args.iteritems(): + if key.startswith('type.'): + type_uri = value + alias = key[5:] + checkAlias(alias) + aliases.addAlias(type_uri, alias) + + for type_uri, alias in aliases.iteritems(): + try: + count_s = ax_args['count.' + alias] + except KeyError: + value = ax_args['value.' + alias] + + if value == u'': + values = [] + else: + values = [value] + else: + count = int(count_s) + values = [] + for i in range(1, count + 1): + value_key = 'value.%s.%d' % (alias, i) + value = ax_args[value_key] + values.append(value) + + self.data[type_uri] = values + + def getSingle(self, type_uri, default=None): + """Get a single value for an attribute. If no value was sent + for this attribute, use the supplied default. If there is more + than one value for this attribute, this method will fail. + + @type type_uri: str + @param type_uri: The URI for the attribute + + @param default: The value to return if the attribute was not + sent in the fetch_response. + + @returns: The value of the attribute in the fetch_response + message, or the default supplied + @rtype: unicode or NoneType + + @raises ValueError: If there is more than one value for this + parameter in the fetch_response message. + @raises KeyError: If the attribute was not sent in this response + """ + values = self.data.get(type_uri) + if not values: + return default + elif len(values) == 1: + return values[0] + else: + raise AXError( + 'More than one value present for %r' % (type_uri,)) + + def get(self, type_uri): + """Get the list of values for this attribute in the + fetch_response. + + XXX: what to do if the values are not present? default + parameter? this is funny because it's always supposed to + return a list, so the default may break that, though it's + provided by the user's code, so it might be okay. If no + default is supplied, should the return be None or []? + + @param type_uri: The URI of the attribute + + @returns: The list of values for this attribute in the + response. May be an empty list. + @rtype: [unicode] + + @raises KeyError: If the attribute was not sent in the response + """ + return self.data[type_uri] + + def count(self, type_uri): + """Get the number of responses for a particular attribute in + this fetch_response message. + + @param type_uri: The URI of the attribute + + @returns: The number of values sent for this attribute + + @raises KeyError: If the attribute was not sent in the + response. KeyError will not be raised if the number of + values was zero. + """ + return len(self.get(type_uri)) + + +class FetchResponse(AXKeyValueMessage): + """A fetch_response attribute exchange message + """ + mode = 'fetch_response' + + def __init__(self, request=None, update_url=None): + """ + @param request: When supplied, I will use namespace aliases + that match those in this request. I will also check to + make sure I do not respond with attributes that were not + requested. + + @type request: L{FetchRequest} + + @param update_url: By default, C{update_url} is taken from the + request. But if you do not supply the request, you may set + the C{update_url} here. + + @type update_url: str + """ + AXKeyValueMessage.__init__(self) + self.update_url = update_url + self.request = request + + def getExtensionArgs(self): + """Serialize this object into arguments in the attribute + exchange namespace + + @returns: The dictionary of unqualified attribute exchange + arguments that represent this fetch_response. + @rtype: {unicode;unicode} + """ + + aliases = NamespaceMap() + + zero_value_types = [] + + if self.request is not None: + # Validate the data in the context of the request (the + # same attributes should be present in each, and the + # counts in the response must be no more than the counts + # in the request) + + for type_uri in self.data: + if type_uri not in self.request: + raise KeyError( + 'Response attribute not present in request: %r' + % (type_uri,)) + + for attr_info in self.request.iterAttrs(): + # Copy the aliases from the request so that reading + # the response in light of the request is easier + if attr_info.alias is None: + aliases.add(attr_info.type_uri) + else: + aliases.addAlias(attr_info.type_uri, attr_info.alias) + + try: + values = self.data[attr_info.type_uri] + except KeyError: + values = [] + zero_value_types.append(attr_info) + + if (attr_info.count != UNLIMITED_VALUES) and \ + (attr_info.count < len(values)): + raise AXError( + 'More than the number of requested values were ' + 'specified for %r' % (attr_info.type_uri,)) + + kv_args = self._getExtensionKVArgs(aliases) + + # Add the KV args into the response with the args that are + # unique to the fetch_response + ax_args = self._newArgs() + + # For each requested attribute, put its type/alias and count + # into the response even if no data were returned. + for attr_info in zero_value_types: + alias = aliases.getAlias(attr_info.type_uri) + kv_args['type.' + alias] = attr_info.type_uri + kv_args['count.' + alias] = '0' + + update_url = ((self.request and self.request.update_url) + or self.update_url) + + if update_url: + ax_args['update_url'] = update_url + + ax_args.update(kv_args) + + return ax_args + + def parseExtensionArgs(self, ax_args): + """@see: {Extension.parseExtensionArgs}""" + super(FetchResponse, self).parseExtensionArgs(ax_args) + self.update_url = ax_args.get('update_url') + + def fromSuccessResponse(cls, success_response, signed=True): + """Construct a FetchResponse object from an OpenID library + SuccessResponse object. + + @param success_response: A successful id_res response object + @type success_response: openid.consumer.consumer.SuccessResponse + + @param signed: Whether non-signed args should be + processsed. If True (the default), only signed arguments + will be processsed. + @type signed: bool + + @returns: A FetchResponse containing the data from the OpenID + message, or None if the SuccessResponse did not contain AX + extension data. + + @raises AXError: when the AX data cannot be parsed. + """ + self = cls() + ax_args = success_response.extensionResponse(self.ns_uri, signed) + + try: + self.parseExtensionArgs(ax_args) + except NotAXMessage, err: + return None + else: + return self + + fromSuccessResponse = classmethod(fromSuccessResponse) + + +class StoreRequest(AXKeyValueMessage): + """A store request attribute exchange message representation + """ + mode = 'store_request' + + def __init__(self, aliases=None): + """ + @param aliases: The namespace aliases to use when making this + store request. Leave as None to use defaults. + """ + super(StoreRequest, self).__init__() + self.aliases = aliases + + def getExtensionArgs(self): + """ + @see: L{Extension.getExtensionArgs} + """ + ax_args = self._newArgs() + kv_args = self._getExtensionKVArgs(self.aliases) + ax_args.update(kv_args) + return ax_args + + +class StoreResponse(AXMessage): + """An indication that the store request was processed along with + this OpenID transaction. + """ + + SUCCESS_MODE = 'store_response_success' + FAILURE_MODE = 'store_response_failure' + + def __init__(self, succeeded=True, error_message=None): + AXMessage.__init__(self) + + if succeeded and error_message is not None: + raise AXError('An error message may only be included in a ' + 'failing fetch response') + if succeeded: + self.mode = self.SUCCESS_MODE + else: + self.mode = self.FAILURE_MODE + + self.error_message = error_message + + def succeeded(self): + """Was this response a success response?""" + return self.mode == self.SUCCESS_MODE + + def getExtensionArgs(self): + """@see: {Extension.getExtensionArgs}""" + ax_args = self._newArgs() + if not self.succeeded() and self.error_message: + ax_args['error'] = self.error_message + + return ax_args diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/extensions/draft/__init__.py b/desktop/core/ext-py/python-openid-2.2.5/openid/extensions/draft/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/extensions/draft/pape2.py b/desktop/core/ext-py/python-openid-2.2.5/openid/extensions/draft/pape2.py new file mode 100644 index 0000000..e732046 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/extensions/draft/pape2.py @@ -0,0 +1,277 @@ +"""An implementation of the OpenID Provider Authentication Policy +Extension 1.0 + +@see: http://openid.net/developers/specs/ + +@since: 2.1.0 +""" + +__all__ = [ + 'Request', + 'Response', + 'ns_uri', + 'AUTH_PHISHING_RESISTANT', + 'AUTH_MULTI_FACTOR', + 'AUTH_MULTI_FACTOR_PHYSICAL', + ] + +from openid.extension import Extension +import re + +ns_uri = "http://specs.openid.net/extensions/pape/1.0" + +AUTH_MULTI_FACTOR_PHYSICAL = \ + 'http://schemas.openid.net/pape/policies/2007/06/multi-factor-physical' +AUTH_MULTI_FACTOR = \ + 'http://schemas.openid.net/pape/policies/2007/06/multi-factor' +AUTH_PHISHING_RESISTANT = \ + 'http://schemas.openid.net/pape/policies/2007/06/phishing-resistant' + +TIME_VALIDATOR = re.compile('^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ$') + +class Request(Extension): + """A Provider Authentication Policy request, sent from a relying + party to a provider + + @ivar preferred_auth_policies: The authentication policies that + the relying party prefers + @type preferred_auth_policies: [str] + + @ivar max_auth_age: The maximum time, in seconds, that the relying + party wants to allow to have elapsed before the user must + re-authenticate + @type max_auth_age: int or NoneType + """ + + ns_alias = 'pape' + + def __init__(self, preferred_auth_policies=None, max_auth_age=None): + super(Request, self).__init__() + if not preferred_auth_policies: + preferred_auth_policies = [] + + self.preferred_auth_policies = preferred_auth_policies + self.max_auth_age = max_auth_age + + def __nonzero__(self): + return bool(self.preferred_auth_policies or + self.max_auth_age is not None) + + def addPolicyURI(self, policy_uri): + """Add an acceptable authentication policy URI to this request + + This method is intended to be used by the relying party to add + acceptable authentication types to the request. + + @param policy_uri: The identifier for the preferred type of + authentication. + @see: http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-01.html#auth_policies + """ + if policy_uri not in self.preferred_auth_policies: + self.preferred_auth_policies.append(policy_uri) + + def getExtensionArgs(self): + """@see: C{L{Extension.getExtensionArgs}} + """ + ns_args = { + 'preferred_auth_policies':' '.join(self.preferred_auth_policies) + } + + if self.max_auth_age is not None: + ns_args['max_auth_age'] = str(self.max_auth_age) + + return ns_args + + def fromOpenIDRequest(cls, request): + """Instantiate a Request object from the arguments in a + C{checkid_*} OpenID message + """ + self = cls() + args = request.message.getArgs(self.ns_uri) + + if args == {}: + return None + + self.parseExtensionArgs(args) + return self + + fromOpenIDRequest = classmethod(fromOpenIDRequest) + + def parseExtensionArgs(self, args): + """Set the state of this request to be that expressed in these + PAPE arguments + + @param args: The PAPE arguments without a namespace + + @rtype: None + + @raises ValueError: When the max_auth_age is not parseable as + an integer + """ + + # preferred_auth_policies is a space-separated list of policy URIs + self.preferred_auth_policies = [] + + policies_str = args.get('preferred_auth_policies') + if policies_str: + for uri in policies_str.split(' '): + if uri not in self.preferred_auth_policies: + self.preferred_auth_policies.append(uri) + + # max_auth_age is base-10 integer number of seconds + max_auth_age_str = args.get('max_auth_age') + self.max_auth_age = None + + if max_auth_age_str: + try: + self.max_auth_age = int(max_auth_age_str) + except ValueError: + pass + + def preferredTypes(self, supported_types): + """Given a list of authentication policy URIs that a provider + supports, this method returns the subsequence of those types + that are preferred by the relying party. + + @param supported_types: A sequence of authentication policy + type URIs that are supported by a provider + + @returns: The sub-sequence of the supported types that are + preferred by the relying party. This list will be ordered + in the order that the types appear in the supported_types + sequence, and may be empty if the provider does not prefer + any of the supported authentication types. + + @returntype: [str] + """ + return filter(self.preferred_auth_policies.__contains__, + supported_types) + +Request.ns_uri = ns_uri + + +class Response(Extension): + """A Provider Authentication Policy response, sent from a provider + to a relying party + """ + + ns_alias = 'pape' + + def __init__(self, auth_policies=None, auth_time=None, + nist_auth_level=None): + super(Response, self).__init__() + if auth_policies: + self.auth_policies = auth_policies + else: + self.auth_policies = [] + + self.auth_time = auth_time + self.nist_auth_level = nist_auth_level + + def addPolicyURI(self, policy_uri): + """Add a authentication policy to this response + + This method is intended to be used by the provider to add a + policy that the provider conformed to when authenticating the user. + + @param policy_uri: The identifier for the preferred type of + authentication. + @see: http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-01.html#auth_policies + """ + if policy_uri not in self.auth_policies: + self.auth_policies.append(policy_uri) + + def fromSuccessResponse(cls, success_response): + """Create a C{L{Response}} object from a successful OpenID + library response + (C{L{openid.consumer.consumer.SuccessResponse}}) response + message + + @param success_response: A SuccessResponse from consumer.complete() + @type success_response: C{L{openid.consumer.consumer.SuccessResponse}} + + @rtype: Response or None + @returns: A provider authentication policy response from the + data that was supplied with the C{id_res} response or None + if the provider sent no signed PAPE response arguments. + """ + self = cls() + + # PAPE requires that the args be signed. + args = success_response.getSignedNS(self.ns_uri) + + # Only try to construct a PAPE response if the arguments were + # signed in the OpenID response. If not, return None. + if args is not None: + self.parseExtensionArgs(args) + return self + else: + return None + + def parseExtensionArgs(self, args, strict=False): + """Parse the provider authentication policy arguments into the + internal state of this object + + @param args: unqualified provider authentication policy + arguments + + @param strict: Whether to raise an exception when bad data is + encountered + + @returns: None. The data is parsed into the internal fields of + this object. + """ + policies_str = args.get('auth_policies') + if policies_str and policies_str != 'none': + self.auth_policies = policies_str.split(' ') + + nist_level_str = args.get('nist_auth_level') + if nist_level_str: + try: + nist_level = int(nist_level_str) + except ValueError: + if strict: + raise ValueError('nist_auth_level must be an integer between ' + 'zero and four, inclusive') + else: + self.nist_auth_level = None + else: + if 0 <= nist_level < 5: + self.nist_auth_level = nist_level + + auth_time = args.get('auth_time') + if auth_time: + if TIME_VALIDATOR.match(auth_time): + self.auth_time = auth_time + elif strict: + raise ValueError("auth_time must be in RFC3339 format") + + fromSuccessResponse = classmethod(fromSuccessResponse) + + def getExtensionArgs(self): + """@see: C{L{Extension.getExtensionArgs}} + """ + if len(self.auth_policies) == 0: + ns_args = { + 'auth_policies':'none', + } + else: + ns_args = { + 'auth_policies':' '.join(self.auth_policies), + } + + if self.nist_auth_level is not None: + if self.nist_auth_level not in range(0, 5): + raise ValueError('nist_auth_level must be an integer between ' + 'zero and four, inclusive') + ns_args['nist_auth_level'] = str(self.nist_auth_level) + + if self.auth_time is not None: + if not TIME_VALIDATOR.match(self.auth_time): + raise ValueError('auth_time must be in RFC3339 format') + + ns_args['auth_time'] = self.auth_time + + return ns_args + +Response.ns_uri = ns_uri diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/extensions/draft/pape5.py b/desktop/core/ext-py/python-openid-2.2.5/openid/extensions/draft/pape5.py new file mode 100644 index 0000000..3bd1ffc --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/extensions/draft/pape5.py @@ -0,0 +1,473 @@ +"""An implementation of the OpenID Provider Authentication Policy +Extension 1.0, Draft 5 + +@see: http://openid.net/developers/specs/ + +@since: 2.1.0 +""" + +__all__ = [ + 'Request', + 'Response', + 'ns_uri', + 'AUTH_PHISHING_RESISTANT', + 'AUTH_MULTI_FACTOR', + 'AUTH_MULTI_FACTOR_PHYSICAL', + 'LEVELS_NIST', + 'LEVELS_JISA', + ] + +from openid.extension import Extension +import warnings +import re + +ns_uri = "http://specs.openid.net/extensions/pape/1.0" + +AUTH_MULTI_FACTOR_PHYSICAL = \ + 'http://schemas.openid.net/pape/policies/2007/06/multi-factor-physical' +AUTH_MULTI_FACTOR = \ + 'http://schemas.openid.net/pape/policies/2007/06/multi-factor' +AUTH_PHISHING_RESISTANT = \ + 'http://schemas.openid.net/pape/policies/2007/06/phishing-resistant' +AUTH_NONE = \ + 'http://schemas.openid.net/pape/policies/2007/06/none' + +TIME_VALIDATOR = re.compile('^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ$') + +LEVELS_NIST = 'http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf' +LEVELS_JISA = 'http://www.jisa.or.jp/spec/auth_level.html' + +class PAPEExtension(Extension): + _default_auth_level_aliases = { + 'nist': LEVELS_NIST, + 'jisa': LEVELS_JISA, + } + + def __init__(self): + self.auth_level_aliases = self._default_auth_level_aliases.copy() + + def _addAuthLevelAlias(self, auth_level_uri, alias=None): + """Add an auth level URI alias to this request. + + @param auth_level_uri: The auth level URI to send in the + request. + + @param alias: The namespace alias to use for this auth level + in this message. May be None if the alias is not + important. + """ + if alias is None: + try: + alias = self._getAlias(auth_level_uri) + except KeyError: + alias = self._generateAlias() + else: + existing_uri = self.auth_level_aliases.get(alias) + if existing_uri is not None and existing_uri != auth_level_uri: + raise KeyError('Attempting to redefine alias %r from %r to %r', + alias, existing_uri, auth_level_uri) + + self.auth_level_aliases[alias] = auth_level_uri + + def _generateAlias(self): + """Return an unused auth level alias""" + for i in xrange(1000): + alias = 'cust%d' % (i,) + if alias not in self.auth_level_aliases: + return alias + + raise RuntimeError('Could not find an unused alias (tried 1000!)') + + def _getAlias(self, auth_level_uri): + """Return the alias for the specified auth level URI. + + @raises KeyError: if no alias is defined + """ + for (alias, existing_uri) in self.auth_level_aliases.iteritems(): + if auth_level_uri == existing_uri: + return alias + + raise KeyError(auth_level_uri) + +class Request(PAPEExtension): + """A Provider Authentication Policy request, sent from a relying + party to a provider + + @ivar preferred_auth_policies: The authentication policies that + the relying party prefers + @type preferred_auth_policies: [str] + + @ivar max_auth_age: The maximum time, in seconds, that the relying + party wants to allow to have elapsed before the user must + re-authenticate + @type max_auth_age: int or NoneType + + @ivar preferred_auth_level_types: Ordered list of authentication + level namespace URIs + + @type preferred_auth_level_types: [str] + """ + + ns_alias = 'pape' + + def __init__(self, preferred_auth_policies=None, max_auth_age=None, + preferred_auth_level_types=None): + super(Request, self).__init__() + if preferred_auth_policies is None: + preferred_auth_policies = [] + + self.preferred_auth_policies = preferred_auth_policies + self.max_auth_age = max_auth_age + self.preferred_auth_level_types = [] + + if preferred_auth_level_types is not None: + for auth_level in preferred_auth_level_types: + self.addAuthLevel(auth_level) + + def __nonzero__(self): + return bool(self.preferred_auth_policies or + self.max_auth_age is not None or + self.preferred_auth_level_types) + + def addPolicyURI(self, policy_uri): + """Add an acceptable authentication policy URI to this request + + This method is intended to be used by the relying party to add + acceptable authentication types to the request. + + @param policy_uri: The identifier for the preferred type of + authentication. + @see: http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-05.html#auth_policies + """ + if policy_uri not in self.preferred_auth_policies: + self.preferred_auth_policies.append(policy_uri) + + def addAuthLevel(self, auth_level_uri, alias=None): + self._addAuthLevelAlias(auth_level_uri, alias) + if auth_level_uri not in self.preferred_auth_level_types: + self.preferred_auth_level_types.append(auth_level_uri) + + def getExtensionArgs(self): + """@see: C{L{Extension.getExtensionArgs}} + """ + ns_args = { + 'preferred_auth_policies':' '.join(self.preferred_auth_policies), + } + + if self.max_auth_age is not None: + ns_args['max_auth_age'] = str(self.max_auth_age) + + if self.preferred_auth_level_types: + preferred_types = [] + + for auth_level_uri in self.preferred_auth_level_types: + alias = self._getAlias(auth_level_uri) + ns_args['auth_level.ns.%s' % (alias,)] = auth_level_uri + preferred_types.append(alias) + + ns_args['preferred_auth_level_types'] = ' '.join(preferred_types) + + return ns_args + + def fromOpenIDRequest(cls, request): + """Instantiate a Request object from the arguments in a + C{checkid_*} OpenID message + """ + self = cls() + args = request.message.getArgs(self.ns_uri) + is_openid1 = request.message.isOpenID1() + + if args == {}: + return None + + self.parseExtensionArgs(args, is_openid1) + return self + + fromOpenIDRequest = classmethod(fromOpenIDRequest) + + def parseExtensionArgs(self, args, is_openid1, strict=False): + """Set the state of this request to be that expressed in these + PAPE arguments + + @param args: The PAPE arguments without a namespace + + @param strict: Whether to raise an exception if the input is + out of spec or otherwise malformed. If strict is false, + malformed input will be ignored. + + @param is_openid1: Whether the input should be treated as part + of an OpenID1 request + + @rtype: None + + @raises ValueError: When the max_auth_age is not parseable as + an integer + """ + + # preferred_auth_policies is a space-separated list of policy URIs + self.preferred_auth_policies = [] + + policies_str = args.get('preferred_auth_policies') + if policies_str: + for uri in policies_str.split(' '): + if uri not in self.preferred_auth_policies: + self.preferred_auth_policies.append(uri) + + # max_auth_age is base-10 integer number of seconds + max_auth_age_str = args.get('max_auth_age') + self.max_auth_age = None + + if max_auth_age_str: + try: + self.max_auth_age = int(max_auth_age_str) + except ValueError: + if strict: + raise + + # Parse auth level information + preferred_auth_level_types = args.get('preferred_auth_level_types') + if preferred_auth_level_types: + aliases = preferred_auth_level_types.strip().split() + + for alias in aliases: + key = 'auth_level.ns.%s' % (alias,) + try: + uri = args[key] + except KeyError: + if is_openid1: + uri = self._default_auth_level_aliases.get(alias) + else: + uri = None + + if uri is None: + if strict: + raise ValueError('preferred auth level %r is not ' + 'defined in this message' % (alias,)) + else: + self.addAuthLevel(uri, alias) + + def preferredTypes(self, supported_types): + """Given a list of authentication policy URIs that a provider + supports, this method returns the subsequence of those types + that are preferred by the relying party. + + @param supported_types: A sequence of authentication policy + type URIs that are supported by a provider + + @returns: The sub-sequence of the supported types that are + preferred by the relying party. This list will be ordered + in the order that the types appear in the supported_types + sequence, and may be empty if the provider does not prefer + any of the supported authentication types. + + @returntype: [str] + """ + return filter(self.preferred_auth_policies.__contains__, + supported_types) + +Request.ns_uri = ns_uri + + +class Response(PAPEExtension): + """A Provider Authentication Policy response, sent from a provider + to a relying party + + @ivar auth_policies: List of authentication policies conformed to + by this OpenID assertion, represented as policy URIs + """ + + ns_alias = 'pape' + + def __init__(self, auth_policies=None, auth_time=None, + auth_levels=None): + super(Response, self).__init__() + if auth_policies: + self.auth_policies = auth_policies + else: + self.auth_policies = [] + + self.auth_time = auth_time + self.auth_levels = {} + + if auth_levels is None: + auth_levels = {} + + for uri, level in auth_levels.iteritems(): + self.setAuthLevel(uri, level) + + def setAuthLevel(self, level_uri, level, alias=None): + """Set the value for the given auth level type. + + @param level: string representation of an authentication level + valid for level_uri + + @param alias: An optional namespace alias for the given auth + level URI. May be omitted if the alias is not + significant. The library will use a reasonable default for + widely-used auth level types. + """ + self._addAuthLevelAlias(level_uri, alias) + self.auth_levels[level_uri] = level + + def getAuthLevel(self, level_uri): + """Return the auth level for the specified auth level + identifier + + @returns: A string that should map to the auth levels defined + for the auth level type + + @raises KeyError: If the auth level type is not present in + this message + """ + return self.auth_levels[level_uri] + + def _getNISTAuthLevel(self): + try: + return int(self.getAuthLevel(LEVELS_NIST)) + except KeyError: + return None + + nist_auth_level = property( + _getNISTAuthLevel, + doc="Backward-compatibility accessor for the NIST auth level") + + def addPolicyURI(self, policy_uri): + """Add a authentication policy to this response + + This method is intended to be used by the provider to add a + policy that the provider conformed to when authenticating the user. + + @param policy_uri: The identifier for the preferred type of + authentication. + @see: http://openid.net/specs/openid-provider-authentication-policy-extension-1_0-01.html#auth_policies + """ + if policy_uri == AUTH_NONE: + raise RuntimeError( + 'To send no policies, do not set any on the response.') + + if policy_uri not in self.auth_policies: + self.auth_policies.append(policy_uri) + + def fromSuccessResponse(cls, success_response): + """Create a C{L{Response}} object from a successful OpenID + library response + (C{L{openid.consumer.consumer.SuccessResponse}}) response + message + + @param success_response: A SuccessResponse from consumer.complete() + @type success_response: C{L{openid.consumer.consumer.SuccessResponse}} + + @rtype: Response or None + @returns: A provider authentication policy response from the + data that was supplied with the C{id_res} response or None + if the provider sent no signed PAPE response arguments. + """ + self = cls() + + # PAPE requires that the args be signed. + args = success_response.getSignedNS(self.ns_uri) + is_openid1 = success_response.isOpenID1() + + # Only try to construct a PAPE response if the arguments were + # signed in the OpenID response. If not, return None. + if args is not None: + self.parseExtensionArgs(args, is_openid1) + return self + else: + return None + + def parseExtensionArgs(self, args, is_openid1, strict=False): + """Parse the provider authentication policy arguments into the + internal state of this object + + @param args: unqualified provider authentication policy + arguments + + @param strict: Whether to raise an exception when bad data is + encountered + + @returns: None. The data is parsed into the internal fields of + this object. + """ + policies_str = args.get('auth_policies') + if policies_str: + auth_policies = policies_str.split(' ') + elif strict: + raise ValueError('Missing auth_policies') + else: + auth_policies = [] + + if (len(auth_policies) > 1 and strict and AUTH_NONE in auth_policies): + raise ValueError('Got some auth policies, as well as the special ' + '"none" URI: %r' % (auth_policies,)) + + if 'none' in auth_policies: + msg = '"none" used as a policy URI (see PAPE draft < 5)' + if strict: + raise ValueError(msg) + else: + warnings.warn(msg, stacklevel=2) + + auth_policies = [u for u in auth_policies + if u not in ['none', AUTH_NONE]] + + self.auth_policies = auth_policies + + for (key, val) in args.iteritems(): + if key.startswith('auth_level.'): + alias = key[11:] + + # skip the already-processed namespace declarations + if alias.startswith('ns.'): + continue + + try: + uri = args['auth_level.ns.%s' % (alias,)] + except KeyError: + if is_openid1: + uri = self._default_auth_level_aliases.get(alias) + else: + uri = None + + if uri is None: + if strict: + raise ValueError( + 'Undefined auth level alias: %r' % (alias,)) + else: + self.setAuthLevel(uri, val, alias) + + auth_time = args.get('auth_time') + if auth_time: + if TIME_VALIDATOR.match(auth_time): + self.auth_time = auth_time + elif strict: + raise ValueError("auth_time must be in RFC3339 format") + + fromSuccessResponse = classmethod(fromSuccessResponse) + + def getExtensionArgs(self): + """@see: C{L{Extension.getExtensionArgs}} + """ + if len(self.auth_policies) == 0: + ns_args = { + 'auth_policies': AUTH_NONE, + } + else: + ns_args = { + 'auth_policies':' '.join(self.auth_policies), + } + + for level_type, level in self.auth_levels.iteritems(): + alias = self._getAlias(level_type) + ns_args['auth_level.ns.%s' % (alias,)] = level_type + ns_args['auth_level.%s' % (alias,)] = str(level) + + if self.auth_time is not None: + if not TIME_VALIDATOR.match(self.auth_time): + raise ValueError('auth_time must be in RFC3339 format') + + ns_args['auth_time'] = self.auth_time + + return ns_args + +Response.ns_uri = ns_uri diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/extensions/sreg.py b/desktop/core/ext-py/python-openid-2.2.5/openid/extensions/sreg.py new file mode 100644 index 0000000..76909d2 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/extensions/sreg.py @@ -0,0 +1,518 @@ +"""Simple registration request and response parsing and object representation + +This module contains objects representing simple registration requests +and responses that can be used with both OpenID relying parties and +OpenID providers. + + 1. The relying party creates a request object and adds it to the + C{L{AuthRequest}} object + before making the C{checkid_} request to the OpenID provider:: + + auth_request.addExtension(SRegRequest(required=['email'])) + + 2. The OpenID provider extracts the simple registration request from + the OpenID request using C{L{SRegRequest.fromOpenIDRequest}}, + gets the user's approval and data, creates a C{L{SRegResponse}} + object and adds it to the C{id_res} response:: + + sreg_req = SRegRequest.fromOpenIDRequest(checkid_request) + # [ get the user's approval and data, informing the user that + # the fields in sreg_response were requested ] + sreg_resp = SRegResponse.extractResponse(sreg_req, user_data) + sreg_resp.toMessage(openid_response.fields) + + 3. The relying party uses C{L{SRegResponse.fromSuccessResponse}} to + extract the data from the OpenID response:: + + sreg_resp = SRegResponse.fromSuccessResponse(success_response) + +@since: 2.0 + +@var sreg_data_fields: The names of the data fields that are listed in + the sreg spec, and a description of them in English + +@var sreg_uri: The preferred URI to use for the simple registration + namespace and XRD Type value +""" + +from openid.message import registerNamespaceAlias, \ + NamespaceAliasRegistrationError +from openid.extension import Extension +from openid import oidutil + +try: + basestring #pylint:disable-msg=W0104 +except NameError: + # For Python 2.2 + basestring = (str, unicode) #pylint:disable-msg=W0622 + +__all__ = [ + 'SRegRequest', + 'SRegResponse', + 'data_fields', + 'ns_uri', + 'ns_uri_1_0', + 'ns_uri_1_1', + 'supportsSReg', + ] + +# The data fields that are listed in the sreg spec +data_fields = { + 'fullname':'Full Name', + 'nickname':'Nickname', + 'dob':'Date of Birth', + 'email':'E-mail Address', + 'gender':'Gender', + 'postcode':'Postal Code', + 'country':'Country', + 'language':'Language', + 'timezone':'Time Zone', + } + +def checkFieldName(field_name): + """Check to see that the given value is a valid simple + registration data field name. + + @raise ValueError: if the field name is not a valid simple + registration data field name + """ + if field_name not in data_fields: + raise ValueError('%r is not a defined simple registration field' % + (field_name,)) + +# URI used in the wild for Yadis documents advertising simple +# registration support +ns_uri_1_0 = 'http://openid.net/sreg/1.0' + +# URI in the draft specification for simple registration 1.1 +# +ns_uri_1_1 = 'http://openid.net/extensions/sreg/1.1' + +# This attribute will always hold the preferred URI to use when adding +# sreg support to an XRDS file or in an OpenID namespace declaration. +ns_uri = ns_uri_1_1 + +try: + registerNamespaceAlias(ns_uri_1_1, 'sreg') +except NamespaceAliasRegistrationError, e: + oidutil.log('registerNamespaceAlias(%r, %r) failed: %s' % (ns_uri_1_1, + 'sreg', str(e),)) + +def supportsSReg(endpoint): + """Does the given endpoint advertise support for simple + registration? + + @param endpoint: The endpoint object as returned by OpenID discovery + @type endpoint: openid.consumer.discover.OpenIDEndpoint + + @returns: Whether an sreg type was advertised by the endpoint + @rtype: bool + """ + return (endpoint.usesExtension(ns_uri_1_1) or + endpoint.usesExtension(ns_uri_1_0)) + +class SRegNamespaceError(ValueError): + """The simple registration namespace was not found and could not + be created using the expected name (there's another extension + using the name 'sreg') + + This is not I{illegal}, for OpenID 2, although it probably + indicates a problem, since it's not expected that other extensions + will re-use the alias that is in use for OpenID 1. + + If this is an OpenID 1 request, then there is no recourse. This + should not happen unless some code has modified the namespaces for + the message that is being processed. + """ + +def getSRegNS(message): + """Extract the simple registration namespace URI from the given + OpenID message. Handles OpenID 1 and 2, as well as both sreg + namespace URIs found in the wild, as well as missing namespace + definitions (for OpenID 1) + + @param message: The OpenID message from which to parse simple + registration fields. This may be a request or response message. + @type message: C{L{openid.message.Message}} + + @returns: the sreg namespace URI for the supplied message. The + message may be modified to define a simple registration + namespace. + @rtype: C{str} + + @raise ValueError: when using OpenID 1 if the message defines + the 'sreg' alias to be something other than a simple + registration type. + """ + # See if there exists an alias for one of the two defined simple + # registration types. + for sreg_ns_uri in [ns_uri_1_1, ns_uri_1_0]: + alias = message.namespaces.getAlias(sreg_ns_uri) + if alias is not None: + break + else: + # There is no alias for either of the types, so try to add + # one. We default to using the modern value (1.1) + sreg_ns_uri = ns_uri_1_1 + try: + message.namespaces.addAlias(ns_uri_1_1, 'sreg') + except KeyError, why: + # An alias for the string 'sreg' already exists, but it's + # defined for something other than simple registration + raise SRegNamespaceError(why[0]) + + # we know that sreg_ns_uri defined, because it's defined in the + # else clause of the loop as well, so disable the warning + return sreg_ns_uri #pylint:disable-msg=W0631 + +class SRegRequest(Extension): + """An object to hold the state of a simple registration request. + + @ivar required: A list of the required fields in this simple + registration request + @type required: [str] + + @ivar optional: A list of the optional fields in this simple + registration request + @type optional: [str] + + @ivar policy_url: The policy URL that was provided with the request + @type policy_url: str or NoneType + + @group Consumer: requestField, requestFields, getExtensionArgs, addToOpenIDRequest + @group Server: fromOpenIDRequest, parseExtensionArgs + """ + + ns_alias = 'sreg' + + def __init__(self, required=None, optional=None, policy_url=None, + sreg_ns_uri=ns_uri): + """Initialize an empty simple registration request""" + Extension.__init__(self) + self.required = [] + self.optional = [] + self.policy_url = policy_url + self.ns_uri = sreg_ns_uri + + if required: + self.requestFields(required, required=True, strict=True) + + if optional: + self.requestFields(optional, required=False, strict=True) + + # Assign getSRegNS to a static method so that it can be + # overridden for testing. + _getSRegNS = staticmethod(getSRegNS) + + def fromOpenIDRequest(cls, request): + """Create a simple registration request that contains the + fields that were requested in the OpenID request with the + given arguments + + @param request: The OpenID request + @type request: openid.server.CheckIDRequest + + @returns: The newly created simple registration request + @rtype: C{L{SRegRequest}} + """ + self = cls() + + # Since we're going to mess with namespace URI mapping, don't + # mutate the object that was passed in. + message = request.message.copy() + + self.ns_uri = self._getSRegNS(message) + args = message.getArgs(self.ns_uri) + self.parseExtensionArgs(args) + + return self + + fromOpenIDRequest = classmethod(fromOpenIDRequest) + + def parseExtensionArgs(self, args, strict=False): + """Parse the unqualified simple registration request + parameters and add them to this object. + + This method is essentially the inverse of + C{L{getExtensionArgs}}. This method restores the serialized simple + registration request fields. + + If you are extracting arguments from a standard OpenID + checkid_* request, you probably want to use C{L{fromOpenIDRequest}}, + which will extract the sreg namespace and arguments from the + OpenID request. This method is intended for cases where the + OpenID server needs more control over how the arguments are + parsed than that method provides. + + >>> args = message.getArgs(ns_uri) + >>> request.parseExtensionArgs(args) + + @param args: The unqualified simple registration arguments + @type args: {str:str} + + @param strict: Whether requests with fields that are not + defined in the simple registration specification should be + tolerated (and ignored) + @type strict: bool + + @returns: None; updates this object + """ + for list_name in ['required', 'optional']: + required = (list_name == 'required') + items = args.get(list_name) + if items: + for field_name in items.split(','): + try: + self.requestField(field_name, required, strict) + except ValueError: + if strict: + raise + + self.policy_url = args.get('policy_url') + + def allRequestedFields(self): + """A list of all of the simple registration fields that were + requested, whether they were required or optional. + + @rtype: [str] + """ + return self.required + self.optional + + def wereFieldsRequested(self): + """Have any simple registration fields been requested? + + @rtype: bool + """ + return bool(self.allRequestedFields()) + + def __contains__(self, field_name): + """Was this field in the request?""" + return (field_name in self.required or + field_name in self.optional) + + def requestField(self, field_name, required=False, strict=False): + """Request the specified field from the OpenID user + + @param field_name: the unqualified simple registration field name + @type field_name: str + + @param required: whether the given field should be presented + to the user as being a required to successfully complete + the request + + @param strict: whether to raise an exception when a field is + added to a request more than once + + @raise ValueError: when the field requested is not a simple + registration field or strict is set and the field was + requested more than once + """ + checkFieldName(field_name) + + if strict: + if field_name in self.required or field_name in self.optional: + raise ValueError('That field has already been requested') + else: + if field_name in self.required: + return + + if field_name in self.optional: + if required: + self.optional.remove(field_name) + else: + return + + if required: + self.required.append(field_name) + else: + self.optional.append(field_name) + + def requestFields(self, field_names, required=False, strict=False): + """Add the given list of fields to the request + + @param field_names: The simple registration data fields to request + @type field_names: [str] + + @param required: Whether these values should be presented to + the user as required + + @param strict: whether to raise an exception when a field is + added to a request more than once + + @raise ValueError: when a field requested is not a simple + registration field or strict is set and a field was + requested more than once + """ + if isinstance(field_names, basestring): + raise TypeError('Fields should be passed as a list of ' + 'strings (not %r)' % (type(field_names),)) + + for field_name in field_names: + self.requestField(field_name, required, strict=strict) + + def getExtensionArgs(self): + """Get a dictionary of unqualified simple registration + arguments representing this request. + + This method is essentially the inverse of + C{L{parseExtensionArgs}}. This method serializes the simple + registration request fields. + + @rtype: {str:str} + """ + args = {} + + if self.required: + args['required'] = ','.join(self.required) + + if self.optional: + args['optional'] = ','.join(self.optional) + + if self.policy_url: + args['policy_url'] = self.policy_url + + return args + +class SRegResponse(Extension): + """Represents the data returned in a simple registration response + inside of an OpenID C{id_res} response. This object will be + created by the OpenID server, added to the C{id_res} response + object, and then extracted from the C{id_res} message by the + Consumer. + + @ivar data: The simple registration data, keyed by the unqualified + simple registration name of the field (i.e. nickname is keyed + by C{'nickname'}) + + @ivar ns_uri: The URI under which the simple registration data was + stored in the response message. + + @group Server: extractResponse + @group Consumer: fromSuccessResponse + @group Read-only dictionary interface: keys, iterkeys, items, iteritems, + __iter__, get, __getitem__, keys, has_key + """ + + ns_alias = 'sreg' + + def __init__(self, data=None, sreg_ns_uri=ns_uri): + Extension.__init__(self) + if data is None: + self.data = {} + else: + self.data = data + + self.ns_uri = sreg_ns_uri + + def extractResponse(cls, request, data): + """Take a C{L{SRegRequest}} and a dictionary of simple + registration values and create a C{L{SRegResponse}} + object containing that data. + + @param request: The simple registration request object + @type request: SRegRequest + + @param data: The simple registration data for this + response, as a dictionary from unqualified simple + registration field name to string (unicode) value. For + instance, the nickname should be stored under the key + 'nickname'. + @type data: {str:str} + + @returns: a simple registration response object + @rtype: SRegResponse + """ + self = cls() + self.ns_uri = request.ns_uri + for field in request.allRequestedFields(): + value = data.get(field) + if value is not None: + self.data[field] = value + return self + + extractResponse = classmethod(extractResponse) + + # Assign getSRegArgs to a static method so that it can be + # overridden for testing + _getSRegNS = staticmethod(getSRegNS) + + def fromSuccessResponse(cls, success_response, signed_only=True): + """Create a C{L{SRegResponse}} object from a successful OpenID + library response + (C{L{openid.consumer.consumer.SuccessResponse}}) response + message + + @param success_response: A SuccessResponse from consumer.complete() + @type success_response: C{L{openid.consumer.consumer.SuccessResponse}} + + @param signed_only: Whether to process only data that was + signed in the id_res message from the server. + @type signed_only: bool + + @rtype: SRegResponse + @returns: A simple registration response containing the data + that was supplied with the C{id_res} response. + """ + self = cls() + self.ns_uri = self._getSRegNS(success_response.message) + if signed_only: + args = success_response.getSignedNS(self.ns_uri) + else: + args = success_response.message.getArgs(self.ns_uri) + + if not args: + return None + + for field_name in data_fields: + if field_name in args: + self.data[field_name] = args[field_name] + + return self + + fromSuccessResponse = classmethod(fromSuccessResponse) + + def getExtensionArgs(self): + """Get the fields to put in the simple registration namespace + when adding them to an id_res message. + + @see: openid.extension + """ + return self.data + + # Read-only dictionary interface + def get(self, field_name, default=None): + """Like dict.get, except that it checks that the field name is + defined by the simple registration specification""" + checkFieldName(field_name) + return self.data.get(field_name, default) + + def items(self): + """All of the data values in this simple registration response + """ + return self.data.items() + + def iteritems(self): + return self.data.iteritems() + + def keys(self): + return self.data.keys() + + def iterkeys(self): + return self.data.iterkeys() + + def has_key(self, key): + return key in self + + def __contains__(self, field_name): + checkFieldName(field_name) + return field_name in self.data + + def __iter__(self): + return iter(self.data) + + def __getitem__(self, field_name): + checkFieldName(field_name) + return self.data[field_name] + + def __nonzero__(self): + return bool(self.data) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/fetchers.py b/desktop/core/ext-py/python-openid-2.2.5/openid/fetchers.py new file mode 100644 index 0000000..944e215 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/fetchers.py @@ -0,0 +1,427 @@ +# -*- test-case-name: openid.test.test_fetchers -*- +""" +This module contains the HTTP fetcher interface and several implementations. +""" + +__all__ = ['fetch', 'getDefaultFetcher', 'setDefaultFetcher', 'HTTPResponse', + 'HTTPFetcher', 'createHTTPFetcher', 'HTTPFetchingError', + 'HTTPError'] + +import urllib2 +import time +import cStringIO +import sys + +import openid +import openid.urinorm + +# Try to import httplib2 for caching support +# http://bitworking.org/projects/httplib2/ +try: + import httplib2 +except ImportError: + # httplib2 not available + httplib2 = None + +# try to import pycurl, which will let us use CurlHTTPFetcher +try: + import pycurl +except ImportError: + pycurl = None + +USER_AGENT = "python-openid/%s (%s)" % (openid.__version__, sys.platform) +MAX_RESPONSE_KB = 1024 + +def fetch(url, body=None, headers=None): + """Invoke the fetch method on the default fetcher. Most users + should need only this method. + + @raises Exception: any exceptions that may be raised by the default fetcher + """ + fetcher = getDefaultFetcher() + return fetcher.fetch(url, body, headers) + +def createHTTPFetcher(): + """Create a default HTTP fetcher instance + + prefers Curl to urllib2.""" + if pycurl is None: + fetcher = Urllib2Fetcher() + else: + fetcher = CurlHTTPFetcher() + + return fetcher + +# Contains the currently set HTTP fetcher. If it is set to None, the +# library will call createHTTPFetcher() to set it. Do not access this +# variable outside of this module. +_default_fetcher = None + +def getDefaultFetcher(): + """Return the default fetcher instance + if no fetcher has been set, it will create a default fetcher. + + @return: the default fetcher + @rtype: HTTPFetcher + """ + global _default_fetcher + + if _default_fetcher is None: + setDefaultFetcher(createHTTPFetcher()) + + return _default_fetcher + +def setDefaultFetcher(fetcher, wrap_exceptions=True): + """Set the default fetcher + + @param fetcher: The fetcher to use as the default HTTP fetcher + @type fetcher: HTTPFetcher + + @param wrap_exceptions: Whether to wrap exceptions thrown by the + fetcher wil HTTPFetchingError so that they may be caught + easier. By default, exceptions will be wrapped. In general, + unwrapped fetchers are useful for debugging of fetching errors + or if your fetcher raises well-known exceptions that you would + like to catch. + @type wrap_exceptions: bool + """ + global _default_fetcher + if fetcher is None or not wrap_exceptions: + _default_fetcher = fetcher + else: + _default_fetcher = ExceptionWrappingFetcher(fetcher) + +def usingCurl(): + """Whether the currently set HTTP fetcher is a Curl HTTP fetcher.""" + return isinstance(getDefaultFetcher(), CurlHTTPFetcher) + +class HTTPResponse(object): + """XXX document attributes""" + headers = None + status = None + body = None + final_url = None + + def __init__(self, final_url=None, status=None, headers=None, body=None): + self.final_url = final_url + self.status = status + self.headers = headers + self.body = body + + def __repr__(self): + return "<%s status %s for %s>" % (self.__class__.__name__, + self.status, + self.final_url) + +class HTTPFetcher(object): + """ + This class is the interface for openid HTTP fetchers. This + interface is only important if you need to write a new fetcher for + some reason. + """ + + def fetch(self, url, body=None, headers=None): + """ + This performs an HTTP POST or GET, following redirects along + the way. If a body is specified, then the request will be a + POST. Otherwise, it will be a GET. + + + @param headers: HTTP headers to include with the request + @type headers: {str:str} + + @return: An object representing the server's HTTP response. If + there are network or protocol errors, an exception will be + raised. HTTP error responses, like 404 or 500, do not + cause exceptions. + + @rtype: L{HTTPResponse} + + @raise Exception: Different implementations will raise + different errors based on the underlying HTTP library. + """ + raise NotImplementedError + +def _allowedURL(url): + return url.startswith('http://') or url.startswith('https://') + +class HTTPFetchingError(Exception): + """Exception that is wrapped around all exceptions that are raised + by the underlying fetcher when using the ExceptionWrappingFetcher + + @ivar why: The exception that caused this exception + """ + def __init__(self, why=None): + Exception.__init__(self, why) + self.why = why + +class ExceptionWrappingFetcher(HTTPFetcher): + """Fetcher that wraps another fetcher, causing all exceptions + + @cvar uncaught_exceptions: Exceptions that should be exposed to the + user if they are raised by the fetch call + """ + + uncaught_exceptions = (SystemExit, KeyboardInterrupt, MemoryError) + + def __init__(self, fetcher): + self.fetcher = fetcher + + def fetch(self, *args, **kwargs): + try: + return self.fetcher.fetch(*args, **kwargs) + except self.uncaught_exceptions: + raise + except: + exc_cls, exc_inst = sys.exc_info()[:2] + if exc_inst is None: + # string exceptions + exc_inst = exc_cls + + raise HTTPFetchingError(why=exc_inst) + +class Urllib2Fetcher(HTTPFetcher): + """An C{L{HTTPFetcher}} that uses urllib2. + """ + + # Parameterized for the benefit of testing frameworks, see + # http://trac.openidenabled.com/trac/ticket/85 + urlopen = staticmethod(urllib2.urlopen) + + def fetch(self, url, body=None, headers=None): + if not _allowedURL(url): + raise ValueError('Bad URL scheme: %r' % (url,)) + + if headers is None: + headers = {} + + headers.setdefault( + 'User-Agent', + "%s Python-urllib/%s" % (USER_AGENT, urllib2.__version__,)) + + req = urllib2.Request(url, data=body, headers=headers) + try: + f = self.urlopen(req) + try: + return self._makeResponse(f) + finally: + f.close() + except urllib2.HTTPError, why: + try: + return self._makeResponse(why) + finally: + why.close() + + def _makeResponse(self, urllib2_response): + resp = HTTPResponse() + resp.body = urllib2_response.read(MAX_RESPONSE_KB * 1024) + resp.final_url = urllib2_response.geturl() + resp.headers = dict(urllib2_response.info().items()) + + if hasattr(urllib2_response, 'code'): + resp.status = urllib2_response.code + else: + resp.status = 200 + + return resp + +class HTTPError(HTTPFetchingError): + """ + This exception is raised by the C{L{CurlHTTPFetcher}} when it + encounters an exceptional situation fetching a URL. + """ + pass + +# XXX: define what we mean by paranoid, and make sure it is. +class CurlHTTPFetcher(HTTPFetcher): + """ + An C{L{HTTPFetcher}} that uses pycurl for fetching. + See U{http://pycurl.sourceforge.net/}. + """ + ALLOWED_TIME = 20 # seconds + + def __init__(self): + HTTPFetcher.__init__(self) + if pycurl is None: + raise RuntimeError('Cannot find pycurl library') + + def _parseHeaders(self, header_file): + header_file.seek(0) + + # Remove the status line from the beginning of the input + unused_http_status_line = header_file.readline().lower () + if unused_http_status_line.startswith('http/1.1 100 '): + unused_http_status_line = header_file.readline() + unused_http_status_line = header_file.readline() + + lines = [line.strip() for line in header_file] + + # and the blank line from the end + empty_line = lines.pop() + if empty_line: + raise HTTPError("No blank line at end of headers: %r" % (line,)) + + headers = {} + for line in lines: + try: + name, value = line.split(':', 1) + except ValueError: + raise HTTPError( + "Malformed HTTP header line in response: %r" % (line,)) + + value = value.strip() + + # HTTP headers are case-insensitive + name = name.lower() + headers[name] = value + + return headers + + def _checkURL(self, url): + # XXX: document that this can be overridden to match desired policy + # XXX: make sure url is well-formed and routeable + return _allowedURL(url) + + def fetch(self, url, body=None, headers=None): + stop = int(time.time()) + self.ALLOWED_TIME + off = self.ALLOWED_TIME + + if headers is None: + headers = {} + + headers.setdefault('User-Agent', + "%s %s" % (USER_AGENT, pycurl.version,)) + + header_list = [] + if headers is not None: + for header_name, header_value in headers.iteritems(): + header_list.append('%s: %s' % (header_name, header_value)) + + c = pycurl.Curl() + try: + c.setopt(pycurl.NOSIGNAL, 1) + + if header_list: + c.setopt(pycurl.HTTPHEADER, header_list) + + # Presence of a body indicates that we should do a POST + if body is not None: + c.setopt(pycurl.POST, 1) + c.setopt(pycurl.POSTFIELDS, body) + + while off > 0: + if not self._checkURL(url): + raise HTTPError("Fetching URL not allowed: %r" % (url,)) + + data = cStringIO.StringIO() + def write_data(chunk): + if data.tell() > 1024*MAX_RESPONSE_KB: + return 0 + else: + return data.write(chunk) + + response_header_data = cStringIO.StringIO() + c.setopt(pycurl.WRITEFUNCTION, write_data) + c.setopt(pycurl.HEADERFUNCTION, response_header_data.write) + c.setopt(pycurl.TIMEOUT, off) + c.setopt(pycurl.URL, openid.urinorm.urinorm(url)) + + c.perform() + + response_headers = self._parseHeaders(response_header_data) + code = c.getinfo(pycurl.RESPONSE_CODE) + if code in [301, 302, 303, 307]: + url = response_headers.get('location') + if url is None: + raise HTTPError( + 'Redirect (%s) returned without a location' % code) + + # Redirects are always GETs + c.setopt(pycurl.POST, 0) + + # There is no way to reset POSTFIELDS to empty and + # reuse the connection, but we only use it once. + else: + resp = HTTPResponse() + resp.headers = response_headers + resp.status = code + resp.final_url = url + resp.body = data.getvalue() + return resp + + off = stop - int(time.time()) + + raise HTTPError("Timed out fetching: %r" % (url,)) + finally: + c.close() + +class HTTPLib2Fetcher(HTTPFetcher): + """A fetcher that uses C{httplib2} for performing HTTP + requests. This implementation supports HTTP caching. + + @see: http://bitworking.org/projects/httplib2/ + """ + + def __init__(self, cache=None): + """@param cache: An object suitable for use as an C{httplib2} + cache. If a string is passed, it is assumed to be a + directory name. + """ + if httplib2 is None: + raise RuntimeError('Cannot find httplib2 library. ' + 'See http://bitworking.org/projects/httplib2/') + + super(HTTPLib2Fetcher, self).__init__() + + # An instance of the httplib2 object that performs HTTP requests + self.httplib2 = httplib2.Http(cache) + + # We want httplib2 to raise exceptions for errors, just like + # the other fetchers. + self.httplib2.force_exception_to_status_code = False + + def fetch(self, url, body=None, headers=None): + """Perform an HTTP request + + @raises Exception: Any exception that can be raised by httplib2 + + @see: C{L{HTTPFetcher.fetch}} + """ + if body: + method = 'POST' + else: + method = 'GET' + + if headers is None: + headers = {} + + # httplib2 doesn't check to make sure that the URL's scheme is + # 'http' so we do it here. + if not (url.startswith('http://') or url.startswith('https://')): + raise ValueError('URL is not a HTTP URL: %r' % (url,)) + + httplib2_response, content = self.httplib2.request( + url, method, body=body, headers=headers) + + # Translate the httplib2 response to our HTTP response abstraction + + # When a 400 is returned, there is no "content-location" + # header set. This seems like a bug to me. I can't think of a + # case where we really care about the final URL when it is an + # error response, but being careful about it can't hurt. + try: + final_url = httplib2_response['content-location'] + except KeyError: + # We're assuming that no redirects occurred + assert not httplib2_response.previous + + # And this should never happen for a successful response + assert httplib2_response.status != 200 + final_url = url + + return HTTPResponse( + body=content, + final_url=final_url, + headers=dict(httplib2_response.items()), + status=httplib2_response.status, + ) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/kvform.py b/desktop/core/ext-py/python-openid-2.2.5/openid/kvform.py new file mode 100644 index 0000000..d875f56 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/kvform.py @@ -0,0 +1,123 @@ +__all__ = ['seqToKV', 'kvToSeq', 'dictToKV', 'kvToDict'] + +from openid import oidutil + +import types + +class KVFormError(ValueError): + pass + +def seqToKV(seq, strict=False): + """Represent a sequence of pairs of strings as newline-terminated + key:value pairs. The pairs are generated in the order given. + + @param seq: The pairs + @type seq: [(str, (unicode|str))] + + @return: A string representation of the sequence + @rtype: str + """ + def err(msg): + formatted = 'seqToKV warning: %s: %r' % (msg, seq) + if strict: + raise KVFormError(formatted) + else: + oidutil.log(formatted) + + lines = [] + for k, v in seq: + if isinstance(k, types.StringType): + k = k.decode('UTF8') + elif not isinstance(k, types.UnicodeType): + err('Converting key to string: %r' % k) + k = str(k) + + if '\n' in k: + raise KVFormError( + 'Invalid input for seqToKV: key contains newline: %r' % (k,)) + + if ':' in k: + raise KVFormError( + 'Invalid input for seqToKV: key contains colon: %r' % (k,)) + + if k.strip() != k: + err('Key has whitespace at beginning or end: %r' % (k,)) + + if isinstance(v, types.StringType): + v = v.decode('UTF8') + elif not isinstance(v, types.UnicodeType): + err('Converting value to string: %r' % (v,)) + v = str(v) + + if '\n' in v: + raise KVFormError( + 'Invalid input for seqToKV: value contains newline: %r' % (v,)) + + if v.strip() != v: + err('Value has whitespace at beginning or end: %r' % (v,)) + + lines.append(k + ':' + v + '\n') + + return ''.join(lines).encode('UTF8') + +def kvToSeq(data, strict=False): + """ + + After one parse, seqToKV and kvToSeq are inverses, with no warnings:: + + seq = kvToSeq(s) + seqToKV(kvToSeq(seq)) == seq + """ + def err(msg): + formatted = 'kvToSeq warning: %s: %r' % (msg, data) + if strict: + raise KVFormError(formatted) + else: + oidutil.log(formatted) + + lines = data.split('\n') + if lines[-1]: + err('Does not end in a newline') + else: + del lines[-1] + + pairs = [] + line_num = 0 + for line in lines: + line_num += 1 + + # Ignore blank lines + if not line.strip(): + continue + + pair = line.split(':', 1) + if len(pair) == 2: + k, v = pair + k_s = k.strip() + if k_s != k: + fmt = ('In line %d, ignoring leading or trailing ' + 'whitespace in key %r') + err(fmt % (line_num, k)) + + if not k_s: + err('In line %d, got empty key' % (line_num,)) + + v_s = v.strip() + if v_s != v: + fmt = ('In line %d, ignoring leading or trailing ' + 'whitespace in value %r') + err(fmt % (line_num, v)) + + pairs.append((k_s.decode('UTF8'), v_s.decode('UTF8'))) + else: + err('Line %d does not contain a colon' % line_num) + + return pairs + +def dictToKV(d): + seq = d.items() + seq.sort() + return seqToKV(seq) + +def kvToDict(s): + return dict(kvToSeq(s)) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/message.py b/desktop/core/ext-py/python-openid-2.2.5/openid/message.py new file mode 100644 index 0000000..472e517 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/message.py @@ -0,0 +1,631 @@ +"""Extension argument processing code +""" +__all__ = ['Message', 'NamespaceMap', 'no_default', 'registerNamespaceAlias', + 'OPENID_NS', 'BARE_NS', 'OPENID1_NS', 'OPENID2_NS', 'SREG_URI', + 'IDENTIFIER_SELECT'] + +import copy +import warnings +import urllib + +from openid import oidutil +from openid import kvform +try: + ElementTree = oidutil.importElementTree() +except ImportError: + # No elementtree found, so give up, but don't fail to import, + # since we have fallbacks. + ElementTree = None + +# This doesn't REALLY belong here, but where is better? +IDENTIFIER_SELECT = 'http://specs.openid.net/auth/2.0/identifier_select' + +# URI for Simple Registration extension, the only commonly deployed +# OpenID 1.x extension, and so a special case +SREG_URI = 'http://openid.net/sreg/1.0' + +# The OpenID 1.X namespace URI +OPENID1_NS = 'http://openid.net/signon/1.0' +THE_OTHER_OPENID1_NS = 'http://openid.net/signon/1.1' + +OPENID1_NAMESPACES = OPENID1_NS, THE_OTHER_OPENID1_NS + +# The OpenID 2.0 namespace URI +OPENID2_NS = 'http://specs.openid.net/auth/2.0' + +# The namespace consisting of pairs with keys that are prefixed with +# "openid." but not in another namespace. +NULL_NAMESPACE = oidutil.Symbol('Null namespace') + +# The null namespace, when it is an allowed OpenID namespace +OPENID_NS = oidutil.Symbol('OpenID namespace') + +# The top-level namespace, excluding all pairs with keys that start +# with "openid." +BARE_NS = oidutil.Symbol('Bare namespace') + +# Limit, in bytes, of identity provider and return_to URLs, including +# response payload. See OpenID 1.1 specification, Appendix D. +OPENID1_URL_LIMIT = 2047 + +# All OpenID protocol fields. Used to check namespace aliases. +OPENID_PROTOCOL_FIELDS = [ + 'ns', 'mode', 'error', 'return_to', 'contact', 'reference', + 'signed', 'assoc_type', 'session_type', 'dh_modulus', 'dh_gen', + 'dh_consumer_public', 'claimed_id', 'identity', 'realm', + 'invalidate_handle', 'op_endpoint', 'response_nonce', 'sig', + 'assoc_handle', 'trust_root', 'openid', + ] + +class UndefinedOpenIDNamespace(ValueError): + """Raised if the generic OpenID namespace is accessed when there + is no OpenID namespace set for this message.""" + +class InvalidOpenIDNamespace(ValueError): + """Raised if openid.ns is not a recognized value. + + For recognized values, see L{Message.allowed_openid_namespaces} + """ + def __str__(self): + s = "Invalid OpenID Namespace" + if self.args: + s += " %r" % (self.args[0],) + return s + + +# Sentinel used for Message implementation to indicate that getArg +# should raise an exception instead of returning a default. +no_default = object() + +# Global namespace / alias registration map. See +# registerNamespaceAlias. +registered_aliases = {} + +class NamespaceAliasRegistrationError(Exception): + """ + Raised when an alias or namespace URI has already been registered. + """ + pass + +def registerNamespaceAlias(namespace_uri, alias): + """ + Registers a (namespace URI, alias) mapping in a global namespace + alias map. Raises NamespaceAliasRegistrationError if either the + namespace URI or alias has already been registered with a + different value. This function is required if you want to use a + namespace with an OpenID 1 message. + """ + global registered_aliases + + if registered_aliases.get(alias) == namespace_uri: + return + + if namespace_uri in registered_aliases.values(): + raise NamespaceAliasRegistrationError, \ + 'Namespace uri %r already registered' % (namespace_uri,) + + if alias in registered_aliases: + raise NamespaceAliasRegistrationError, \ + 'Alias %r already registered' % (alias,) + + registered_aliases[alias] = namespace_uri + +class Message(object): + """ + In the implementation of this object, None represents the global + namespace as well as a namespace with no key. + + @cvar namespaces: A dictionary specifying specific + namespace-URI to alias mappings that should be used when + generating namespace aliases. + + @ivar ns_args: two-level dictionary of the values in this message, + grouped by namespace URI. The first level is the namespace + URI. + """ + + allowed_openid_namespaces = [OPENID1_NS, THE_OTHER_OPENID1_NS, OPENID2_NS] + + def __init__(self, openid_namespace=None): + """Create an empty Message. + + @raises InvalidOpenIDNamespace: if openid_namespace is not in + L{Message.allowed_openid_namespaces} + """ + self.args = {} + self.namespaces = NamespaceMap() + if openid_namespace is None: + self._openid_ns_uri = None + else: + implicit = openid_namespace in OPENID1_NAMESPACES + self.setOpenIDNamespace(openid_namespace, implicit) + + def fromPostArgs(cls, args): + """Construct a Message containing a set of POST arguments. + + """ + self = cls() + + # Partition into "openid." args and bare args + openid_args = {} + for key, value in args.items(): + if isinstance(value, list): + raise TypeError("query dict must have one value for each key, " + "not lists of values. Query is %r" % (args,)) + + + try: + prefix, rest = key.split('.', 1) + except ValueError: + prefix = None + + if prefix != 'openid': + self.args[(BARE_NS, key)] = value + else: + openid_args[rest] = value + + self._fromOpenIDArgs(openid_args) + + return self + + fromPostArgs = classmethod(fromPostArgs) + + def fromOpenIDArgs(cls, openid_args): + """Construct a Message from a parsed KVForm message. + + @raises InvalidOpenIDNamespace: if openid.ns is not in + L{Message.allowed_openid_namespaces} + """ + self = cls() + self._fromOpenIDArgs(openid_args) + return self + + fromOpenIDArgs = classmethod(fromOpenIDArgs) + + def _fromOpenIDArgs(self, openid_args): + ns_args = [] + + # Resolve namespaces + for rest, value in openid_args.iteritems(): + try: + ns_alias, ns_key = rest.split('.', 1) + except ValueError: + ns_alias = NULL_NAMESPACE + ns_key = rest + + if ns_alias == 'ns': + self.namespaces.addAlias(value, ns_key) + elif ns_alias == NULL_NAMESPACE and ns_key == 'ns': + # null namespace + self.setOpenIDNamespace(value, False) + else: + ns_args.append((ns_alias, ns_key, value)) + + # Implicitly set an OpenID namespace definition (OpenID 1) + if not self.getOpenIDNamespace(): + self.setOpenIDNamespace(OPENID1_NS, True) + + # Actually put the pairs into the appropriate namespaces + for (ns_alias, ns_key, value) in ns_args: + ns_uri = self.namespaces.getNamespaceURI(ns_alias) + if ns_uri is None: + # we found a namespaced arg without a namespace URI defined + ns_uri = self._getDefaultNamespace(ns_alias) + if ns_uri is None: + ns_uri = self.getOpenIDNamespace() + ns_key = '%s.%s' % (ns_alias, ns_key) + else: + self.namespaces.addAlias(ns_uri, ns_alias, implicit=True) + + self.setArg(ns_uri, ns_key, value) + + def _getDefaultNamespace(self, mystery_alias): + """OpenID 1 compatibility: look for a default namespace URI to + use for this alias.""" + global registered_aliases + # Only try to map an alias to a default if it's an + # OpenID 1.x message. + if self.isOpenID1(): + return registered_aliases.get(mystery_alias) + else: + return None + + def setOpenIDNamespace(self, openid_ns_uri, implicit): + """Set the OpenID namespace URI used in this message. + + @raises InvalidOpenIDNamespace: if the namespace is not in + L{Message.allowed_openid_namespaces} + """ + if openid_ns_uri not in self.allowed_openid_namespaces: + raise InvalidOpenIDNamespace(openid_ns_uri) + + self.namespaces.addAlias(openid_ns_uri, NULL_NAMESPACE, implicit) + self._openid_ns_uri = openid_ns_uri + + def getOpenIDNamespace(self): + return self._openid_ns_uri + + def isOpenID1(self): + return self.getOpenIDNamespace() in OPENID1_NAMESPACES + + def isOpenID2(self): + return self.getOpenIDNamespace() == OPENID2_NS + + def fromKVForm(cls, kvform_string): + """Create a Message from a KVForm string""" + return cls.fromOpenIDArgs(kvform.kvToDict(kvform_string)) + + fromKVForm = classmethod(fromKVForm) + + def copy(self): + return copy.deepcopy(self) + + def toPostArgs(self): + """Return all arguments with openid. in front of namespaced arguments. + """ + args = {} + + # Add namespace definitions to the output + for ns_uri, alias in self.namespaces.iteritems(): + if self.namespaces.isImplicit(ns_uri): + continue + if alias == NULL_NAMESPACE: + ns_key = 'openid.ns' + else: + ns_key = 'openid.ns.' + alias + args[ns_key] = ns_uri + + for (ns_uri, ns_key), value in self.args.iteritems(): + key = self.getKey(ns_uri, ns_key) + args[key] = value.encode('UTF-8') + + return args + + def toArgs(self): + """Return all namespaced arguments, failing if any + non-namespaced arguments exist.""" + # FIXME - undocumented exception + post_args = self.toPostArgs() + kvargs = {} + for k, v in post_args.iteritems(): + if not k.startswith('openid.'): + raise ValueError( + 'This message can only be encoded as a POST, because it ' + 'contains arguments that are not prefixed with "openid."') + else: + kvargs[k[7:]] = v + + return kvargs + + def toFormMarkup(self, action_url, form_tag_attrs=None, + submit_text="Continue"): + """Generate HTML form markup that contains the values in this + message, to be HTTP POSTed as x-www-form-urlencoded UTF-8. + + @param action_url: The URL to which the form will be POSTed + @type action_url: str + + @param form_tag_attrs: Dictionary of attributes to be added to + the form tag. 'accept-charset' and 'enctype' have defaults + that can be overridden. If a value is supplied for + 'action' or 'method', it will be replaced. + @type form_tag_attrs: {unicode: unicode} + + @param submit_text: The text that will appear on the submit + button for this form. + @type submit_text: unicode + + @returns: A string containing (X)HTML markup for a form that + encodes the values in this Message object. + @rtype: str or unicode + """ + if ElementTree is None: + raise RuntimeError('This function requires ElementTree.') + + assert action_url is not None + + form = ElementTree.Element('form') + + if form_tag_attrs: + for name, attr in form_tag_attrs.iteritems(): + form.attrib[name] = attr + + form.attrib['action'] = action_url + form.attrib['method'] = 'post' + form.attrib['accept-charset'] = 'UTF-8' + form.attrib['enctype'] = 'application/x-www-form-urlencoded' + + for name, value in self.toPostArgs().iteritems(): + attrs = {'type': 'hidden', + 'name': name, + 'value': value} + form.append(ElementTree.Element('input', attrs)) + + submit = ElementTree.Element( + 'input', {'type':'submit', 'value':submit_text}) + form.append(submit) + + return ElementTree.tostring(form) + + def toURL(self, base_url): + """Generate a GET URL with the parameters in this message + attached as query parameters.""" + return oidutil.appendArgs(base_url, self.toPostArgs()) + + def toKVForm(self): + """Generate a KVForm string that contains the parameters in + this message. This will fail if the message contains arguments + outside of the 'openid.' prefix. + """ + return kvform.dictToKV(self.toArgs()) + + def toURLEncoded(self): + """Generate an x-www-urlencoded string""" + args = self.toPostArgs().items() + args.sort() + return urllib.urlencode(args) + + def _fixNS(self, namespace): + """Convert an input value into the internally used values of + this object + + @param namespace: The string or constant to convert + @type namespace: str or unicode or BARE_NS or OPENID_NS + """ + if namespace == OPENID_NS: + if self._openid_ns_uri is None: + raise UndefinedOpenIDNamespace('OpenID namespace not set') + else: + namespace = self._openid_ns_uri + + if namespace != BARE_NS and type(namespace) not in [str, unicode]: + raise TypeError( + "Namespace must be BARE_NS, OPENID_NS or a string. got %r" + % (namespace,)) + + if namespace != BARE_NS and ':' not in namespace: + fmt = 'OpenID 2.0 namespace identifiers SHOULD be URIs. Got %r' + warnings.warn(fmt % (namespace,), DeprecationWarning) + + if namespace == 'sreg': + fmt = 'Using %r instead of "sreg" as namespace' + warnings.warn(fmt % (SREG_URI,), DeprecationWarning,) + return SREG_URI + + return namespace + + def hasKey(self, namespace, ns_key): + namespace = self._fixNS(namespace) + return (namespace, ns_key) in self.args + + def getKey(self, namespace, ns_key): + """Get the key for a particular namespaced argument""" + namespace = self._fixNS(namespace) + if namespace == BARE_NS: + return ns_key + + ns_alias = self.namespaces.getAlias(namespace) + + # No alias is defined, so no key can exist + if ns_alias is None: + return None + + if ns_alias == NULL_NAMESPACE: + tail = ns_key + else: + tail = '%s.%s' % (ns_alias, ns_key) + + return 'openid.' + tail + + def getArg(self, namespace, key, default=None): + """Get a value for a namespaced key. + + @param namespace: The namespace in the message for this key + @type namespace: str + + @param key: The key to get within this namespace + @type key: str + + @param default: The value to use if this key is absent from + this message. Using the special value + openid.message.no_default will result in this method + raising a KeyError instead of returning the default. + + @rtype: str or the type of default + @raises KeyError: if default is no_default + @raises UndefinedOpenIDNamespace: if the message has not yet + had an OpenID namespace set + """ + namespace = self._fixNS(namespace) + args_key = (namespace, key) + try: + return self.args[args_key] + except KeyError: + if default is no_default: + raise KeyError((namespace, key)) + else: + return default + + def getArgs(self, namespace): + """Get the arguments that are defined for this namespace URI + + @returns: mapping from namespaced keys to values + @returntype: dict + """ + namespace = self._fixNS(namespace) + return dict([ + (ns_key, value) + for ((pair_ns, ns_key), value) + in self.args.iteritems() + if pair_ns == namespace + ]) + + def updateArgs(self, namespace, updates): + """Set multiple key/value pairs in one call + + @param updates: The values to set + @type updates: {unicode:unicode} + """ + namespace = self._fixNS(namespace) + for k, v in updates.iteritems(): + self.setArg(namespace, k, v) + + def setArg(self, namespace, key, value): + """Set a single argument in this namespace""" + assert key is not None + assert value is not None + namespace = self._fixNS(namespace) + self.args[(namespace, key)] = value + if not (namespace is BARE_NS): + self.namespaces.add(namespace) + + def delArg(self, namespace, key): + namespace = self._fixNS(namespace) + del self.args[(namespace, key)] + + def __repr__(self): + return "<%s.%s %r>" % (self.__class__.__module__, + self.__class__.__name__, + self.args) + + def __eq__(self, other): + return self.args == other.args + + + def __ne__(self, other): + return not (self == other) + + + def getAliasedArg(self, aliased_key, default=None): + if aliased_key == 'ns': + return self.getOpenIDNamespace() + + if aliased_key.startswith('ns.'): + uri = self.namespaces.getNamespaceURI(aliased_key[3:]) + if uri is None: + if default == no_default: + raise KeyError + else: + return default + else: + return uri + + try: + alias, key = aliased_key.split('.', 1) + except ValueError: + # need more than x values to unpack + ns = None + else: + ns = self.namespaces.getNamespaceURI(alias) + + if ns is None: + key = aliased_key + ns = self.getOpenIDNamespace() + + return self.getArg(ns, key, default) + +class NamespaceMap(object): + """Maintains a bijective map between namespace uris and aliases. + """ + def __init__(self): + self.alias_to_namespace = {} + self.namespace_to_alias = {} + self.implicit_namespaces = [] + + def getAlias(self, namespace_uri): + return self.namespace_to_alias.get(namespace_uri) + + def getNamespaceURI(self, alias): + return self.alias_to_namespace.get(alias) + + def iterNamespaceURIs(self): + """Return an iterator over the namespace URIs""" + return iter(self.namespace_to_alias) + + def iterAliases(self): + """Return an iterator over the aliases""" + return iter(self.alias_to_namespace) + + def iteritems(self): + """Iterate over the mapping + + @returns: iterator of (namespace_uri, alias) + """ + return self.namespace_to_alias.iteritems() + + def addAlias(self, namespace_uri, desired_alias, implicit=False): + """Add an alias from this namespace URI to the desired alias + """ + # Check that desired_alias is not an openid protocol field as + # per the spec. + assert desired_alias not in OPENID_PROTOCOL_FIELDS, \ + "%r is not an allowed namespace alias" % (desired_alias,) + + # Check that desired_alias does not contain a period as per + # the spec. + if type(desired_alias) in [str, unicode]: + assert '.' not in desired_alias, \ + "%r must not contain a dot" % (desired_alias,) + + # Check that there is not a namespace already defined for + # the desired alias + current_namespace_uri = self.alias_to_namespace.get(desired_alias) + if (current_namespace_uri is not None + and current_namespace_uri != namespace_uri): + + fmt = ('Cannot map %r to alias %r. ' + '%r is already mapped to alias %r') + + msg = fmt % ( + namespace_uri, + desired_alias, + current_namespace_uri, + desired_alias) + raise KeyError(msg) + + # Check that there is not already a (different) alias for + # this namespace URI + alias = self.namespace_to_alias.get(namespace_uri) + if alias is not None and alias != desired_alias: + fmt = ('Cannot map %r to alias %r. ' + 'It is already mapped to alias %r') + raise KeyError(fmt % (namespace_uri, desired_alias, alias)) + + assert (desired_alias == NULL_NAMESPACE or + type(desired_alias) in [str, unicode]), repr(desired_alias) + assert namespace_uri not in self.implicit_namespaces + self.alias_to_namespace[desired_alias] = namespace_uri + self.namespace_to_alias[namespace_uri] = desired_alias + if implicit: + self.implicit_namespaces.append(namespace_uri) + return desired_alias + + def add(self, namespace_uri): + """Add this namespace URI to the mapping, without caring what + alias it ends up with""" + # See if this namespace is already mapped to an alias + alias = self.namespace_to_alias.get(namespace_uri) + if alias is not None: + return alias + + # Fall back to generating a numerical alias + i = 0 + while True: + alias = 'ext' + str(i) + try: + self.addAlias(namespace_uri, alias) + except KeyError: + i += 1 + else: + return alias + + assert False, "Not reached" + + def isDefined(self, namespace_uri): + return namespace_uri in self.namespace_to_alias + + def __contains__(self, namespace_uri): + return self.isDefined(namespace_uri) + + def isImplicit(self, namespace_uri): + return namespace_uri in self.implicit_namespaces diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/oidutil.py b/desktop/core/ext-py/python-openid-2.2.5/openid/oidutil.py new file mode 100644 index 0000000..5246d16 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/oidutil.py @@ -0,0 +1,190 @@ +"""This module contains general utility code that is used throughout +the library. + +For users of this library, the C{L{log}} function is probably the most +interesting. +""" + +__all__ = ['log', 'appendArgs', 'toBase64', 'fromBase64', 'autoSubmitHTML'] + +import binascii +import sys +import urlparse + +from urllib import urlencode + +elementtree_modules = [ + 'lxml.etree', + 'xml.etree.cElementTree', + 'xml.etree.ElementTree', + 'cElementTree', + 'elementtree.ElementTree', + ] + +def autoSubmitHTML(form, title='OpenID transaction in progress'): + return """ + + + %s + + +%s + + + +""" % (title, form) + +def importElementTree(module_names=None): + """Find a working ElementTree implementation, trying the standard + places that such a thing might show up. + + >>> ElementTree = importElementTree() + + @param module_names: The names of modules to try to use as + ElementTree. Defaults to C{L{elementtree_modules}} + + @returns: An ElementTree module + """ + if module_names is None: + module_names = elementtree_modules + + for mod_name in module_names: + try: + ElementTree = __import__(mod_name, None, None, ['unused']) + except ImportError: + pass + else: + # Make sure it can actually parse XML + try: + ElementTree.XML('') + except (SystemExit, MemoryError, AssertionError): + raise + except: + why = sys.exc_info()[1] + log('Not using ElementTree library %r because it failed to ' + 'parse a trivial document: %s' % (mod_name, why)) + else: + return ElementTree + else: + raise ImportError('No ElementTree library found. ' + 'You may need to install one. ' + 'Tried importing %r' % (module_names,) + ) + +def log(message, level=0): + """Handle a log message from the OpenID library. + + This implementation writes the string it to C{sys.stderr}, + followed by a newline. + + Currently, the library does not use the second parameter to this + function, but that may change in the future. + + To install your own logging hook:: + + from openid import oidutil + + def myLoggingFunction(message, level): + ... + + oidutil.log = myLoggingFunction + + @param message: A string containing a debugging message from the + OpenID library + @type message: str + + @param level: The severity of the log message. This parameter is + currently unused, but in the future, the library may indicate + more important information with a higher level value. + @type level: int or None + + @returns: Nothing. + """ + + sys.stderr.write(message) + sys.stderr.write('\n') + +def appendArgs(url, args): + """Append query arguments to a HTTP(s) URL. If the URL already has + query arguemtns, these arguments will be added, and the existing + arguments will be preserved. Duplicate arguments will not be + detected or collapsed (both will appear in the output). + + @param url: The url to which the arguments will be appended + @type url: str + + @param args: The query arguments to add to the URL. If a + dictionary is passed, the items will be sorted before + appending them to the URL. If a sequence of pairs is passed, + the order of the sequence will be preserved. + @type args: A dictionary from string to string, or a sequence of + pairs of strings. + + @returns: The URL with the parameters added + @rtype: str + """ + if hasattr(args, 'items'): + args = args.items() + args.sort() + else: + args = list(args) + + if len(args) == 0: + return url + + if '?' in url: + sep = '&' + else: + sep = '?' + + # Map unicode to UTF-8 if present. Do not make any assumptions + # about the encodings of plain bytes (str). + i = 0 + for k, v in args: + if type(k) is not str: + k = k.encode('UTF-8') + + if type(v) is not str: + v = v.encode('UTF-8') + + args[i] = (k, v) + i += 1 + + return '%s%s%s' % (url, sep, urlencode(args)) + +def toBase64(s): + """Represent string s as base64, omitting newlines""" + return binascii.b2a_base64(s)[:-1] + +def fromBase64(s): + try: + return binascii.a2b_base64(s) + except binascii.Error, why: + # Convert to a common exception type + raise ValueError(why[0]) + +class Symbol(object): + """This class implements an object that compares equal to others + of the same type that have the same name. These are distict from + str or unicode objects. + """ + + def __init__(self, name): + self.name = name + + def __eq__(self, other): + return type(self) is type(other) and self.name == other.name + + def __ne__(self, other): + return not (self == other) + + def __hash__(self): + return hash((self.__class__, self.name)) + + def __repr__(self): + return '' % (self.name,) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/server/__init__.py b/desktop/core/ext-py/python-openid-2.2.5/openid/server/__init__.py new file mode 100644 index 0000000..c8fde25 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/server/__init__.py @@ -0,0 +1,6 @@ +""" +This package contains the portions of the library used only when +implementing an OpenID server. See L{openid.server.server}. +""" + +__all__ = ['server', 'trustroot'] diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/server/server.py b/desktop/core/ext-py/python-openid-2.2.5/openid/server/server.py new file mode 100644 index 0000000..f653b04 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/server/server.py @@ -0,0 +1,1849 @@ +# -*- test-case-name: openid.test.test_server -*- +"""OpenID server protocol and logic. + +Overview +======== + + An OpenID server must perform three tasks: + + 1. Examine the incoming request to determine its nature and validity. + + 2. Make a decision about how to respond to this request. + + 3. Format the response according to the protocol. + + The first and last of these tasks may performed by + the L{decodeRequest} and + L{encodeResponse} methods of the + L{Server} object. Who gets to do the intermediate task -- deciding + how to respond to the request -- will depend on what type of request it + is. + + If it's a request to authenticate a user (a X{C{checkid_setup}} or + X{C{checkid_immediate}} request), you need to decide if you will assert + that this user may claim the identity in question. Exactly how you do + that is a matter of application policy, but it generally involves making + sure the user has an account with your system and is logged in, checking + to see if that identity is hers to claim, and verifying with the user that + she does consent to releasing that information to the party making the + request. + + Examine the properties of the L{CheckIDRequest} object, optionally + check L{CheckIDRequest.returnToVerified}, and and when you've come + to a decision, form a response by calling L{CheckIDRequest.answer}. + + Other types of requests relate to establishing associations between client + and server and verifying the authenticity of previous communications. + L{Server} contains all the logic and data necessary to respond to + such requests; just pass the request to L{Server.handleRequest}. + + +OpenID Extensions +================= + + Do you want to provide other information for your users + in addition to authentication? Version 2.0 of the OpenID + protocol allows consumers to add extensions to their requests. + For example, with sites using the U{Simple Registration + Extension}, + a user can agree to have their nickname and e-mail address sent to a + site when they sign up. + + Since extensions do not change the way OpenID authentication works, + code to handle extension requests may be completely separate from the + L{OpenIDRequest} class here. But you'll likely want data sent back by + your extension to be signed. L{OpenIDResponse} provides methods with + which you can add data to it which can be signed with the other data in + the OpenID signature. + + For example:: + + # when request is a checkid_* request + response = request.answer(True) + # this will a signed 'openid.sreg.timezone' parameter to the response + # as well as a namespace declaration for the openid.sreg namespace + response.fields.setArg('http://openid.net/sreg/1.0', 'timezone', 'America/Los_Angeles') + + There are helper modules for a number of extensions, including + L{Attribute Exchange}, + L{PAPE}, and + L{Simple Registration} in the L{openid.extensions} + package. + +Stores +====== + + The OpenID server needs to maintain state between requests in order + to function. Its mechanism for doing this is called a store. The + store interface is defined in C{L{openid.store.interface.OpenIDStore}}. + Additionally, several concrete store implementations are provided, so that + most sites won't need to implement a custom store. For a store backed + by flat files on disk, see C{L{openid.store.filestore.FileOpenIDStore}}. + For stores based on MySQL or SQLite, see the C{L{openid.store.sqlstore}} + module. + + +Upgrading +========= + +From 1.0 to 1.1 +--------------- + + The keys by which a server looks up associations in its store have changed + in version 1.2 of this library. If your store has entries created from + version 1.0 code, you should empty it. + +From 1.1 to 2.0 +--------------- + + One of the additions to the OpenID protocol was a specified nonce + format for one-way nonces. As a result, the nonce table in the store + has changed. You'll need to run contrib/upgrade-store-1.1-to-2.0 to + upgrade your store, or you'll encounter errors about the wrong number + of columns in the oid_nonces table. + + If you've written your own custom store or code that interacts + directly with it, you'll need to review the change notes in + L{openid.store.interface}. + +@group Requests: OpenIDRequest, AssociateRequest, CheckIDRequest, + CheckAuthRequest + +@group Responses: OpenIDResponse + +@group HTTP Codes: HTTP_OK, HTTP_REDIRECT, HTTP_ERROR + +@group Response Encodings: ENCODE_KVFORM, ENCODE_HTML_FORM, ENCODE_URL +""" + +import time, warnings +from copy import deepcopy + +from openid import cryptutil +from openid import oidutil +from openid import kvform +from openid.dh import DiffieHellman +from openid.store.nonce import mkNonce +from openid.server.trustroot import TrustRoot, verifyReturnTo +from openid.association import Association, default_negotiator, getSecretSize +from openid.message import Message, InvalidOpenIDNamespace, \ + OPENID_NS, OPENID2_NS, IDENTIFIER_SELECT, OPENID1_URL_LIMIT +from openid.urinorm import urinorm + +HTTP_OK = 200 +HTTP_REDIRECT = 302 +HTTP_ERROR = 400 + +BROWSER_REQUEST_MODES = ['checkid_setup', 'checkid_immediate'] + +ENCODE_KVFORM = ('kvform',) +ENCODE_URL = ('URL/redirect',) +ENCODE_HTML_FORM = ('HTML form',) + +UNUSED = None + +class OpenIDRequest(object): + """I represent an incoming OpenID request. + + @cvar mode: the C{X{openid.mode}} of this request. + @type mode: str + """ + mode = None + + +class CheckAuthRequest(OpenIDRequest): + """A request to verify the validity of a previous response. + + @cvar mode: "X{C{check_authentication}}" + @type mode: str + + @ivar assoc_handle: The X{association handle} the response was signed with. + @type assoc_handle: str + @ivar signed: The message with the signature which wants checking. + @type signed: L{Message} + + @ivar invalidate_handle: An X{association handle} the client is asking + about the validity of. Optional, may be C{None}. + @type invalidate_handle: str + + @see: U{OpenID Specs, Mode: check_authentication + } + """ + mode = "check_authentication" + + required_fields = ["identity", "return_to", "response_nonce"] + + def __init__(self, assoc_handle, signed, invalidate_handle=None): + """Construct me. + + These parameters are assigned directly as class attributes, see + my L{class documentation} for their descriptions. + + @type assoc_handle: str + @type signed: L{Message} + @type invalidate_handle: str + """ + self.assoc_handle = assoc_handle + self.signed = signed + self.invalidate_handle = invalidate_handle + self.namespace = OPENID2_NS + + + def fromMessage(klass, message, op_endpoint=UNUSED): + """Construct me from an OpenID Message. + + @param message: An OpenID check_authentication Message + @type message: L{openid.message.Message} + + @returntype: L{CheckAuthRequest} + """ + self = klass.__new__(klass) + self.message = message + self.namespace = message.getOpenIDNamespace() + self.assoc_handle = message.getArg(OPENID_NS, 'assoc_handle') + self.sig = message.getArg(OPENID_NS, 'sig') + + if (self.assoc_handle is None or + self.sig is None): + fmt = "%s request missing required parameter from message %s" + raise ProtocolError( + message, text=fmt % (self.mode, message)) + + self.invalidate_handle = message.getArg(OPENID_NS, 'invalidate_handle') + + self.signed = message.copy() + # openid.mode is currently check_authentication because + # that's the mode of this request. But the signature + # was made on something with a different openid.mode. + # http://article.gmane.org/gmane.comp.web.openid.general/537 + if self.signed.hasKey(OPENID_NS, "mode"): + self.signed.setArg(OPENID_NS, "mode", "id_res") + + return self + + fromMessage = classmethod(fromMessage) + + def answer(self, signatory): + """Respond to this request. + + Given a L{Signatory}, I can check the validity of the signature and + the X{C{invalidate_handle}}. + + @param signatory: The L{Signatory} to use to check the signature. + @type signatory: L{Signatory} + + @returns: A response with an X{C{is_valid}} (and, if + appropriate X{C{invalidate_handle}}) field. + @returntype: L{OpenIDResponse} + """ + is_valid = signatory.verify(self.assoc_handle, self.signed) + # Now invalidate that assoc_handle so it this checkAuth message cannot + # be replayed. + signatory.invalidate(self.assoc_handle, dumb=True) + response = OpenIDResponse(self) + valid_str = (is_valid and "true") or "false" + response.fields.setArg(OPENID_NS, 'is_valid', valid_str) + + if self.invalidate_handle: + assoc = signatory.getAssociation(self.invalidate_handle, dumb=False) + if not assoc: + response.fields.setArg( + OPENID_NS, 'invalidate_handle', self.invalidate_handle) + return response + + + def __str__(self): + if self.invalidate_handle: + ih = " invalidate? %r" % (self.invalidate_handle,) + else: + ih = "" + s = "<%s handle: %r sig: %r: signed: %r%s>" % ( + self.__class__.__name__, self.assoc_handle, + self.sig, self.signed, ih) + return s + + +class PlainTextServerSession(object): + """An object that knows how to handle association requests with no + session type. + + @cvar session_type: The session_type for this association + session. There is no type defined for plain-text in the OpenID + specification, so we use 'no-encryption'. + @type session_type: str + + @see: U{OpenID Specs, Mode: associate + } + @see: AssociateRequest + """ + session_type = 'no-encryption' + allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256'] + + def fromMessage(cls, unused_request): + return cls() + + fromMessage = classmethod(fromMessage) + + def answer(self, secret): + return {'mac_key': oidutil.toBase64(secret)} + + +class DiffieHellmanSHA1ServerSession(object): + """An object that knows how to handle association requests with the + Diffie-Hellman session type. + + @cvar session_type: The session_type for this association + session. + @type session_type: str + + @ivar dh: The Diffie-Hellman algorithm values for this request + @type dh: DiffieHellman + + @ivar consumer_pubkey: The public key sent by the consumer in the + associate request + @type consumer_pubkey: long + + @see: U{OpenID Specs, Mode: associate + } + @see: AssociateRequest + """ + session_type = 'DH-SHA1' + hash_func = staticmethod(cryptutil.sha1) + allowed_assoc_types = ['HMAC-SHA1'] + + def __init__(self, dh, consumer_pubkey): + self.dh = dh + self.consumer_pubkey = consumer_pubkey + + def fromMessage(cls, message): + """ + @param message: The associate request message + @type message: openid.message.Message + + @returntype: L{DiffieHellmanSHA1ServerSession} + + @raises ProtocolError: When parameters required to establish the + session are missing. + """ + dh_modulus = message.getArg(OPENID_NS, 'dh_modulus') + dh_gen = message.getArg(OPENID_NS, 'dh_gen') + if (dh_modulus is None and dh_gen is not None or + dh_gen is None and dh_modulus is not None): + + if dh_modulus is None: + missing = 'modulus' + else: + missing = 'generator' + + raise ProtocolError(message, + 'If non-default modulus or generator is ' + 'supplied, both must be supplied. Missing %s' + % (missing,)) + + if dh_modulus or dh_gen: + dh_modulus = cryptutil.base64ToLong(dh_modulus) + dh_gen = cryptutil.base64ToLong(dh_gen) + dh = DiffieHellman(dh_modulus, dh_gen) + else: + dh = DiffieHellman.fromDefaults() + + consumer_pubkey = message.getArg(OPENID_NS, 'dh_consumer_public') + if consumer_pubkey is None: + raise ProtocolError(message, "Public key for DH-SHA1 session " + "not found in message %s" % (message,)) + + consumer_pubkey = cryptutil.base64ToLong(consumer_pubkey) + + return cls(dh, consumer_pubkey) + + fromMessage = classmethod(fromMessage) + + def answer(self, secret): + mac_key = self.dh.xorSecret(self.consumer_pubkey, + secret, + self.hash_func) + return { + 'dh_server_public': cryptutil.longToBase64(self.dh.public), + 'enc_mac_key': oidutil.toBase64(mac_key), + } + +class DiffieHellmanSHA256ServerSession(DiffieHellmanSHA1ServerSession): + session_type = 'DH-SHA256' + hash_func = staticmethod(cryptutil.sha256) + allowed_assoc_types = ['HMAC-SHA256'] + +class AssociateRequest(OpenIDRequest): + """A request to establish an X{association}. + + @cvar mode: "X{C{check_authentication}}" + @type mode: str + + @ivar assoc_type: The type of association. The protocol currently only + defines one value for this, "X{C{HMAC-SHA1}}". + @type assoc_type: str + + @ivar session: An object that knows how to handle association + requests of a certain type. + + @see: U{OpenID Specs, Mode: associate + } + """ + + mode = "associate" + + session_classes = { + 'no-encryption': PlainTextServerSession, + 'DH-SHA1': DiffieHellmanSHA1ServerSession, + 'DH-SHA256': DiffieHellmanSHA256ServerSession, + } + + def __init__(self, session, assoc_type): + """Construct me. + + The session is assigned directly as a class attribute. See my + L{class documentation} for its description. + """ + super(AssociateRequest, self).__init__() + self.session = session + self.assoc_type = assoc_type + self.namespace = OPENID2_NS + + + def fromMessage(klass, message, op_endpoint=UNUSED): + """Construct me from an OpenID Message. + + @param message: The OpenID associate request + @type message: openid.message.Message + + @returntype: L{AssociateRequest} + """ + if message.isOpenID1(): + session_type = message.getArg(OPENID_NS, 'session_type') + if session_type == 'no-encryption': + oidutil.log('Received OpenID 1 request with a no-encryption ' + 'assocaition session type. Continuing anyway.') + elif not session_type: + session_type = 'no-encryption' + else: + session_type = message.getArg(OPENID2_NS, 'session_type') + if session_type is None: + raise ProtocolError(message, + text="session_type missing from request") + + try: + session_class = klass.session_classes[session_type] + except KeyError: + raise ProtocolError(message, + "Unknown session type %r" % (session_type,)) + + try: + session = session_class.fromMessage(message) + except ValueError, why: + raise ProtocolError(message, 'Error parsing %s session: %s' % + (session_class.session_type, why[0])) + + assoc_type = message.getArg(OPENID_NS, 'assoc_type', 'HMAC-SHA1') + if assoc_type not in session.allowed_assoc_types: + fmt = 'Session type %s does not support association type %s' + raise ProtocolError(message, fmt % (session_type, assoc_type)) + + self = klass(session, assoc_type) + self.message = message + self.namespace = message.getOpenIDNamespace() + return self + + fromMessage = classmethod(fromMessage) + + def answer(self, assoc): + """Respond to this request with an X{association}. + + @param assoc: The association to send back. + @type assoc: L{openid.association.Association} + + @returns: A response with the association information, encrypted + to the consumer's X{public key} if appropriate. + @returntype: L{OpenIDResponse} + """ + response = OpenIDResponse(self) + response.fields.updateArgs(OPENID_NS, { + 'expires_in': '%d' % (assoc.getExpiresIn(),), + 'assoc_type': self.assoc_type, + 'assoc_handle': assoc.handle, + }) + response.fields.updateArgs(OPENID_NS, + self.session.answer(assoc.secret)) + + if not (self.session.session_type == 'no-encryption' and + self.message.isOpenID1()): + # The session type "no-encryption" did not have a name + # in OpenID v1, it was just omitted. + response.fields.setArg( + OPENID_NS, 'session_type', self.session.session_type) + + return response + + def answerUnsupported(self, message, preferred_association_type=None, + preferred_session_type=None): + """Respond to this request indicating that the association + type or association session type is not supported.""" + if self.message.isOpenID1(): + raise ProtocolError(self.message) + + response = OpenIDResponse(self) + response.fields.setArg(OPENID_NS, 'error_code', 'unsupported-type') + response.fields.setArg(OPENID_NS, 'error', message) + + if preferred_association_type: + response.fields.setArg( + OPENID_NS, 'assoc_type', preferred_association_type) + + if preferred_session_type: + response.fields.setArg( + OPENID_NS, 'session_type', preferred_session_type) + + return response + +class CheckIDRequest(OpenIDRequest): + """A request to confirm the identity of a user. + + This class handles requests for openid modes X{C{checkid_immediate}} + and X{C{checkid_setup}}. + + @cvar mode: "X{C{checkid_immediate}}" or "X{C{checkid_setup}}" + @type mode: str + + @ivar immediate: Is this an immediate-mode request? + @type immediate: bool + + @ivar identity: The OP-local identifier being checked. + @type identity: str + + @ivar claimed_id: The claimed identifier. Not present in OpenID 1.x + messages. + @type claimed_id: str + + @ivar trust_root: "Are you Frank?" asks the checkid request. "Who wants + to know?" C{trust_root}, that's who. This URL identifies the party + making the request, and the user will use that to make her decision + about what answer she trusts them to have. Referred to as "realm" in + OpenID 2.0. + @type trust_root: str + + @ivar return_to: The URL to send the user agent back to to reply to this + request. + @type return_to: str + + @ivar assoc_handle: Provided in smart mode requests, a handle for a + previously established association. C{None} for dumb mode requests. + @type assoc_handle: str + """ + + def __init__(self, identity, return_to, trust_root=None, immediate=False, + assoc_handle=None, op_endpoint=None, claimed_id=None): + """Construct me. + + These parameters are assigned directly as class attributes, see + my L{class documentation} for their descriptions. + + @raises MalformedReturnURL: When the C{return_to} URL is not a URL. + """ + self.assoc_handle = assoc_handle + self.identity = identity + self.claimed_id = claimed_id or identity + self.return_to = return_to + self.trust_root = trust_root or return_to + self.op_endpoint = op_endpoint + assert self.op_endpoint is not None + if immediate: + self.immediate = True + self.mode = "checkid_immediate" + else: + self.immediate = False + self.mode = "checkid_setup" + + if self.return_to is not None and \ + not TrustRoot.parse(self.return_to): + raise MalformedReturnURL(None, self.return_to) + if not self.trustRootValid(): + raise UntrustedReturnURL(None, self.return_to, self.trust_root) + self.message = None + + def _getNamespace(self): + warnings.warn('The "namespace" attribute of CheckIDRequest objects ' + 'is deprecated. Use "message.getOpenIDNamespace()" ' + 'instead', DeprecationWarning, stacklevel=2) + return self.message.getOpenIDNamespace() + + namespace = property(_getNamespace) + + def fromMessage(klass, message, op_endpoint): + """Construct me from an OpenID message. + + @raises ProtocolError: When not all required parameters are present + in the message. + + @raises MalformedReturnURL: When the C{return_to} URL is not a URL. + + @raises UntrustedReturnURL: When the C{return_to} URL is outside + the C{trust_root}. + + @param message: An OpenID checkid_* request Message + @type message: openid.message.Message + + @param op_endpoint: The endpoint URL of the server that this + message was sent to. + @type op_endpoint: str + + @returntype: L{CheckIDRequest} + """ + self = klass.__new__(klass) + self.message = message + self.op_endpoint = op_endpoint + mode = message.getArg(OPENID_NS, 'mode') + if mode == "checkid_immediate": + self.immediate = True + self.mode = "checkid_immediate" + else: + self.immediate = False + self.mode = "checkid_setup" + + self.return_to = message.getArg(OPENID_NS, 'return_to') + if message.isOpenID1() and not self.return_to: + fmt = "Missing required field 'return_to' from %r" + raise ProtocolError(message, text=fmt % (message,)) + + self.identity = message.getArg(OPENID_NS, 'identity') + self.claimed_id = message.getArg(OPENID_NS, 'claimed_id') + if message.isOpenID1(): + if self.identity is None: + s = "OpenID 1 message did not contain openid.identity" + raise ProtocolError(message, text=s) + else: + if self.identity and not self.claimed_id: + s = ("OpenID 2.0 message contained openid.identity but not " + "claimed_id") + raise ProtocolError(message, text=s) + elif self.claimed_id and not self.identity: + s = ("OpenID 2.0 message contained openid.claimed_id but not " + "identity") + raise ProtocolError(message, text=s) + + # There's a case for making self.trust_root be a TrustRoot + # here. But if TrustRoot isn't currently part of the "public" API, + # I'm not sure it's worth doing. + + if message.isOpenID1(): + trust_root_param = 'trust_root' + else: + trust_root_param = 'realm' + + # Using 'or' here is slightly different than sending a default + # argument to getArg, as it will treat no value and an empty + # string as equivalent. + self.trust_root = (message.getArg(OPENID_NS, trust_root_param) + or self.return_to) + + if not message.isOpenID1(): + if self.return_to is self.trust_root is None: + raise ProtocolError(message, "openid.realm required when " + + "openid.return_to absent") + + self.assoc_handle = message.getArg(OPENID_NS, 'assoc_handle') + + # Using TrustRoot.parse here is a bit misleading, as we're not + # parsing return_to as a trust root at all. However, valid URLs + # are valid trust roots, so we can use this to get an idea if it + # is a valid URL. Not all trust roots are valid return_to URLs, + # however (particularly ones with wildcards), so this is still a + # little sketchy. + if self.return_to is not None and \ + not TrustRoot.parse(self.return_to): + raise MalformedReturnURL(message, self.return_to) + + # I first thought that checking to see if the return_to is within + # the trust_root is premature here, a logic-not-decoding thing. But + # it was argued that this is really part of data validation. A + # request with an invalid trust_root/return_to is broken regardless of + # application, right? + if not self.trustRootValid(): + raise UntrustedReturnURL(message, self.return_to, self.trust_root) + + return self + + fromMessage = classmethod(fromMessage) + + def idSelect(self): + """Is the identifier to be selected by the IDP? + + @returntype: bool + """ + # So IDPs don't have to import the constant + return self.identity == IDENTIFIER_SELECT + + def trustRootValid(self): + """Is my return_to under my trust_root? + + @returntype: bool + """ + if not self.trust_root: + return True + tr = TrustRoot.parse(self.trust_root) + if tr is None: + raise MalformedTrustRoot(self.message, self.trust_root) + + if self.return_to is not None: + return tr.validateURL(self.return_to) + else: + return True + + def returnToVerified(self): + """Does the relying party publish the return_to URL for this + response under the realm? It is up to the provider to set a + policy for what kinds of realms should be allowed. This + return_to URL verification reduces vulnerability to data-theft + attacks based on open proxies, cross-site-scripting, or open + redirectors. + + This check should only be performed after making sure that the + return_to URL matches the realm. + + @see: L{trustRootValid} + + @raises openid.yadis.discover.DiscoveryFailure: if the realm + URL does not support Yadis discovery (and so does not + support the verification process). + + @raises openid.fetchers.HTTPFetchingError: if the realm URL + is not reachable. When this is the case, the RP may be hosted + on the user's intranet. + + @returntype: bool + + @returns: True if the realm publishes a document with the + return_to URL listed + + @since: 2.1.0 + """ + return verifyReturnTo(self.trust_root, self.return_to) + + def answer(self, allow, server_url=None, identity=None, claimed_id=None): + """Respond to this request. + + @param allow: Allow this user to claim this identity, and allow the + consumer to have this information? + @type allow: bool + + @param server_url: DEPRECATED. Passing C{op_endpoint} to the + L{Server} constructor makes this optional. + + When an OpenID 1.x immediate mode request does not succeed, + it gets back a URL where the request may be carried out + in a not-so-immediate fashion. Pass my URL in here (the + fully qualified address of this server's endpoint, i.e. + C{http://example.com/server}), and I will use it as a base for the + URL for a new request. + + Optional for requests where C{CheckIDRequest.immediate} is C{False} + or C{allow} is C{True}. + + @type server_url: str + + @param identity: The OP-local identifier to answer with. Only for use + when the relying party requested identifier selection. + @type identity: str or None + + @param claimed_id: The claimed identifier to answer with, for use + with identifier selection in the case where the claimed identifier + and the OP-local identifier differ, i.e. when the claimed_id uses + delegation. + + If C{identity} is provided but this is not, C{claimed_id} will + default to the value of C{identity}. When answering requests + that did not ask for identifier selection, the response + C{claimed_id} will default to that of the request. + + This parameter is new in OpenID 2.0. + @type claimed_id: str or None + + @returntype: L{OpenIDResponse} + + @change: Version 2.0 deprecates C{server_url} and adds C{claimed_id}. + + @raises NoReturnError: when I do not have a return_to. + """ + assert self.message is not None + + if not self.return_to: + raise NoReturnToError + + if not server_url: + if not self.message.isOpenID1() and not self.op_endpoint: + # In other words, that warning I raised in Server.__init__? + # You should pay attention to it now. + raise RuntimeError("%s should be constructed with op_endpoint " + "to respond to OpenID 2.0 messages." % + (self,)) + server_url = self.op_endpoint + + if allow: + mode = 'id_res' + elif self.message.isOpenID1(): + if self.immediate: + mode = 'id_res' + else: + mode = 'cancel' + else: + if self.immediate: + mode = 'setup_needed' + else: + mode = 'cancel' + + response = OpenIDResponse(self) + + if claimed_id and self.message.isOpenID1(): + namespace = self.message.getOpenIDNamespace() + raise VersionError("claimed_id is new in OpenID 2.0 and not " + "available for %s" % (namespace,)) + + if allow: + if self.identity == IDENTIFIER_SELECT: + if not identity: + raise ValueError( + "This request uses IdP-driven identifier selection." + "You must supply an identifier in the response.") + response_identity = identity + response_claimed_id = claimed_id or identity + + elif self.identity: + if identity and (self.identity != identity): + normalized_request_identity = urinorm(self.identity) + normalized_answer_identity = urinorm(identity) + + if (normalized_request_identity != + normalized_answer_identity): + raise ValueError( + "Request was for identity %r, cannot reply " + "with identity %r" % (self.identity, identity)) + + # The "identity" value in the response shall always be + # the same as that in the request, otherwise the RP is + # likely to not validate the response. + response_identity = self.identity + response_claimed_id = self.claimed_id + else: + if identity: + raise ValueError( + "This request specified no identity and you " + "supplied %r" % (identity,)) + response_identity = None + + if self.message.isOpenID1() and response_identity is None: + raise ValueError( + "Request was an OpenID 1 request, so response must " + "include an identifier." + ) + + response.fields.updateArgs(OPENID_NS, { + 'mode': mode, + 'return_to': self.return_to, + 'response_nonce': mkNonce(), + }) + + if server_url: + response.fields.setArg(OPENID_NS, 'op_endpoint', server_url) + + if response_identity is not None: + response.fields.setArg( + OPENID_NS, 'identity', response_identity) + if self.message.isOpenID2(): + response.fields.setArg( + OPENID_NS, 'claimed_id', response_claimed_id) + else: + response.fields.setArg(OPENID_NS, 'mode', mode) + if self.immediate: + if self.message.isOpenID1() and not server_url: + raise ValueError("setup_url is required for allow=False " + "in OpenID 1.x immediate mode.") + # Make a new request just like me, but with immediate=False. + setup_request = self.__class__( + self.identity, self.return_to, self.trust_root, + immediate=False, assoc_handle=self.assoc_handle, + op_endpoint=self.op_endpoint, claimed_id=self.claimed_id) + + # XXX: This API is weird. + setup_request.message = self.message + + setup_url = setup_request.encodeToURL(server_url) + response.fields.setArg(OPENID_NS, 'user_setup_url', setup_url) + + return response + + + def encodeToURL(self, server_url): + """Encode this request as a URL to GET. + + @param server_url: The URL of the OpenID server to make this request of. + @type server_url: str + + @returntype: str + + @raises NoReturnError: when I do not have a return_to. + """ + if not self.return_to: + raise NoReturnToError + + # Imported from the alternate reality where these classes are used + # in both the client and server code, so Requests are Encodable too. + # That's right, code imported from alternate realities all for the + # love of you, id_res/user_setup_url. + q = {'mode': self.mode, + 'identity': self.identity, + 'claimed_id': self.claimed_id, + 'return_to': self.return_to} + if self.trust_root: + if self.message.isOpenID1(): + q['trust_root'] = self.trust_root + else: + q['realm'] = self.trust_root + if self.assoc_handle: + q['assoc_handle'] = self.assoc_handle + + response = Message(self.message.getOpenIDNamespace()) + response.updateArgs(OPENID_NS, q) + return response.toURL(server_url) + + + def getCancelURL(self): + """Get the URL to cancel this request. + + Useful for creating a "Cancel" button on a web form so that operation + can be carried out directly without another trip through the server. + + (Except you probably want to make another trip through the server so + that it knows that the user did make a decision. Or you could simulate + this method by doing C{.answer(False).encodeToURL()}) + + @returntype: str + @returns: The return_to URL with openid.mode = cancel. + + @raises NoReturnError: when I do not have a return_to. + """ + if not self.return_to: + raise NoReturnToError + + if self.immediate: + raise ValueError("Cancel is not an appropriate response to " + "immediate mode requests.") + + response = Message(self.message.getOpenIDNamespace()) + response.setArg(OPENID_NS, 'mode', 'cancel') + return response.toURL(self.return_to) + + + def __repr__(self): + return '<%s id:%r im:%s tr:%r ah:%r>' % (self.__class__.__name__, + self.identity, + self.immediate, + self.trust_root, + self.assoc_handle) + + + +class OpenIDResponse(object): + """I am a response to an OpenID request. + + @ivar request: The request I respond to. + @type request: L{OpenIDRequest} + + @ivar fields: My parameters as a dictionary with each key mapping to + one value. Keys are parameter names with no leading "C{openid.}". + e.g. "C{identity}" and "C{mac_key}", never "C{openid.identity}". + @type fields: L{openid.message.Message} + + @ivar signed: The names of the fields which should be signed. + @type signed: list of str + """ + + # Implementer's note: In a more symmetric client/server + # implementation, there would be more types of OpenIDResponse + # object and they would have validated attributes according to the + # type of response. But as it is, Response objects in a server are + # basically write-only, their only job is to go out over the wire, + # so this is just a loose wrapper around OpenIDResponse.fields. + + def __init__(self, request): + """Make a response to an L{OpenIDRequest}. + + @type request: L{OpenIDRequest} + """ + self.request = request + self.fields = Message(request.namespace) + + def __str__(self): + return "%s for %s: %s" % ( + self.__class__.__name__, + self.request.__class__.__name__, + self.fields) + + + def toFormMarkup(self, form_tag_attrs=None): + """Returns the form markup for this response. + + @param form_tag_attrs: Dictionary of attributes to be added to + the form tag. 'accept-charset' and 'enctype' have defaults + that can be overridden. If a value is supplied for + 'action' or 'method', it will be replaced. + + @returntype: str + + @since: 2.1.0 + """ + return self.fields.toFormMarkup(self.request.return_to, + form_tag_attrs=form_tag_attrs) + + def toHTML(self, form_tag_attrs=None): + """Returns an HTML document that auto-submits the form markup + for this response. + + @returntype: str + + @see: toFormMarkup + + @since: 2.1.? + """ + return oidutil.autoSubmitHTML(self.toFormMarkup(form_tag_attrs)) + + def renderAsForm(self): + """Returns True if this response's encoding is + ENCODE_HTML_FORM. Convenience method for server authors. + + @returntype: bool + + @since: 2.1.0 + """ + return self.whichEncoding() == ENCODE_HTML_FORM + + + def needsSigning(self): + """Does this response require signing? + + @returntype: bool + """ + return self.fields.getArg(OPENID_NS, 'mode') == 'id_res' + + + # implements IEncodable + + def whichEncoding(self): + """How should I be encoded? + + @returns: one of ENCODE_URL, ENCODE_HTML_FORM, or ENCODE_KVFORM. + + @change: 2.1.0 added the ENCODE_HTML_FORM response. + """ + if self.request.mode in BROWSER_REQUEST_MODES: + if self.fields.getOpenIDNamespace() == OPENID2_NS and \ + len(self.encodeToURL()) > OPENID1_URL_LIMIT: + return ENCODE_HTML_FORM + else: + return ENCODE_URL + else: + return ENCODE_KVFORM + + + def encodeToURL(self): + """Encode a response as a URL for the user agent to GET. + + You will generally use this URL with a HTTP redirect. + + @returns: A URL to direct the user agent back to. + @returntype: str + """ + return self.fields.toURL(self.request.return_to) + + + def addExtension(self, extension_response): + """ + Add an extension response to this response message. + + @param extension_response: An object that implements the + extension interface for adding arguments to an OpenID + message. + @type extension_response: L{openid.extension} + + @returntype: None + """ + extension_response.toMessage(self.fields) + + + def encodeToKVForm(self): + """Encode a response in key-value colon/newline format. + + This is a machine-readable format used to respond to messages which + came directly from the consumer and not through the user agent. + + @see: OpenID Specs, + U{Key-Value Colon/Newline format} + + @returntype: str + """ + return self.fields.toKVForm() + + + +class WebResponse(object): + """I am a response to an OpenID request in terms a web server understands. + + I generally come from an L{Encoder}, either directly or from + L{Server.encodeResponse}. + + @ivar code: The HTTP code of this response. + @type code: int + + @ivar headers: Headers to include in this response. + @type headers: dict + + @ivar body: The body of this response. + @type body: str + """ + + def __init__(self, code=HTTP_OK, headers=None, body=""): + """Construct me. + + These parameters are assigned directly as class attributes, see + my L{class documentation} for their descriptions. + """ + self.code = code + if headers is not None: + self.headers = headers + else: + self.headers = {} + self.body = body + + + +class Signatory(object): + """I sign things. + + I also check signatures. + + All my state is encapsulated in an + L{OpenIDStore}, which means + I'm not generally pickleable but I am easy to reconstruct. + + @cvar SECRET_LIFETIME: The number of seconds a secret remains valid. + @type SECRET_LIFETIME: int + """ + + SECRET_LIFETIME = 14 * 24 * 60 * 60 # 14 days, in seconds + + # keys have a bogus server URL in them because the filestore + # really does expect that key to be a URL. This seems a little + # silly for the server store, since I expect there to be only one + # server URL. + _normal_key = 'http://localhost/|normal' + _dumb_key = 'http://localhost/|dumb' + + + def __init__(self, store): + """Create a new Signatory. + + @param store: The back-end where my associations are stored. + @type store: L{openid.store.interface.OpenIDStore} + """ + assert store is not None + self.store = store + + + def verify(self, assoc_handle, message): + """Verify that the signature for some data is valid. + + @param assoc_handle: The handle of the association used to sign the + data. + @type assoc_handle: str + + @param message: The signed message to verify + @type message: openid.message.Message + + @returns: C{True} if the signature is valid, C{False} if not. + @returntype: bool + """ + assoc = self.getAssociation(assoc_handle, dumb=True) + if not assoc: + oidutil.log("failed to get assoc with handle %r to verify " + "message %r" + % (assoc_handle, message)) + return False + + try: + valid = assoc.checkMessageSignature(message) + except ValueError, ex: + oidutil.log("Error in verifying %s with %s: %s" % (message, + assoc, + ex)) + return False + return valid + + + def sign(self, response): + """Sign a response. + + I take a L{OpenIDResponse}, create a signature for everything + in its L{signed} list, and return a new + copy of the response object with that signature included. + + @param response: A response to sign. + @type response: L{OpenIDResponse} + + @returns: A signed copy of the response. + @returntype: L{OpenIDResponse} + """ + signed_response = deepcopy(response) + assoc_handle = response.request.assoc_handle + if assoc_handle: + # normal mode + # disabling expiration check because even if the association + # is expired, we still need to know some properties of the + # association so that we may preserve those properties when + # creating the fallback association. + assoc = self.getAssociation(assoc_handle, dumb=False, + checkExpiration=False) + + if not assoc or assoc.expiresIn <= 0: + # fall back to dumb mode + signed_response.fields.setArg( + OPENID_NS, 'invalidate_handle', assoc_handle) + assoc_type = assoc and assoc.assoc_type or 'HMAC-SHA1' + if assoc and assoc.expiresIn <= 0: + # now do the clean-up that the disabled checkExpiration + # code didn't get to do. + self.invalidate(assoc_handle, dumb=False) + assoc = self.createAssociation(dumb=True, assoc_type=assoc_type) + else: + # dumb mode. + assoc = self.createAssociation(dumb=True) + + try: + signed_response.fields = assoc.signMessage(signed_response.fields) + except kvform.KVFormError, err: + raise EncodingError(response, explanation=str(err)) + return signed_response + + + def createAssociation(self, dumb=True, assoc_type='HMAC-SHA1'): + """Make a new association. + + @param dumb: Is this association for a dumb-mode transaction? + @type dumb: bool + + @param assoc_type: The type of association to create. Currently + there is only one type defined, C{HMAC-SHA1}. + @type assoc_type: str + + @returns: the new association. + @returntype: L{openid.association.Association} + """ + secret = cryptutil.getBytes(getSecretSize(assoc_type)) + uniq = oidutil.toBase64(cryptutil.getBytes(4)) + handle = '{%s}{%x}{%s}' % (assoc_type, int(time.time()), uniq) + + assoc = Association.fromExpiresIn( + self.SECRET_LIFETIME, handle, secret, assoc_type) + + if dumb: + key = self._dumb_key + else: + key = self._normal_key + self.store.storeAssociation(key, assoc) + return assoc + + + def getAssociation(self, assoc_handle, dumb, checkExpiration=True): + """Get the association with the specified handle. + + @type assoc_handle: str + + @param dumb: Is this association used with dumb mode? + @type dumb: bool + + @returns: the association, or None if no valid association with that + handle was found. + @returntype: L{openid.association.Association} + """ + # Hmm. We've created an interface that deals almost entirely with + # assoc_handles. The only place outside the Signatory that uses this + # (and thus the only place that ever sees Association objects) is + # when creating a response to an association request, as it must have + # the association's secret. + + if assoc_handle is None: + raise ValueError("assoc_handle must not be None") + + if dumb: + key = self._dumb_key + else: + key = self._normal_key + assoc = self.store.getAssociation(key, assoc_handle) + if assoc is not None and assoc.expiresIn <= 0: + oidutil.log("requested %sdumb key %r is expired (by %s seconds)" % + ((not dumb) and 'not-' or '', + assoc_handle, assoc.expiresIn)) + if checkExpiration: + self.store.removeAssociation(key, assoc_handle) + assoc = None + return assoc + + + def invalidate(self, assoc_handle, dumb): + """Invalidates the association with the given handle. + + @type assoc_handle: str + + @param dumb: Is this association used with dumb mode? + @type dumb: bool + """ + if dumb: + key = self._dumb_key + else: + key = self._normal_key + self.store.removeAssociation(key, assoc_handle) + + + +class Encoder(object): + """I encode responses in to L{WebResponses}. + + If you don't like L{WebResponses}, you can do + your own handling of L{OpenIDResponses} with + L{OpenIDResponse.whichEncoding}, L{OpenIDResponse.encodeToURL}, and + L{OpenIDResponse.encodeToKVForm}. + """ + + responseFactory = WebResponse + + + def encode(self, response): + """Encode a response to a L{WebResponse}. + + @raises EncodingError: When I can't figure out how to encode this + message. + """ + encode_as = response.whichEncoding() + if encode_as == ENCODE_KVFORM: + wr = self.responseFactory(body=response.encodeToKVForm()) + if isinstance(response, Exception): + wr.code = HTTP_ERROR + elif encode_as == ENCODE_URL: + location = response.encodeToURL() + wr = self.responseFactory(code=HTTP_REDIRECT, + headers={'location': location}) + elif encode_as == ENCODE_HTML_FORM: + wr = self.responseFactory(code=HTTP_OK, + body=response.toFormMarkup()) + else: + # Can't encode this to a protocol message. You should probably + # render it to HTML and show it to the user. + raise EncodingError(response) + return wr + + + +class SigningEncoder(Encoder): + """I encode responses in to L{WebResponses}, signing them when required. + """ + + def __init__(self, signatory): + """Create a L{SigningEncoder}. + + @param signatory: The L{Signatory} I will make signatures with. + @type signatory: L{Signatory} + """ + self.signatory = signatory + + + def encode(self, response): + """Encode a response to a L{WebResponse}, signing it first if appropriate. + + @raises EncodingError: When I can't figure out how to encode this + message. + + @raises AlreadySigned: When this response is already signed. + + @returntype: L{WebResponse} + """ + # the isinstance is a bit of a kludge... it means there isn't really + # an adapter to make the interfaces quite match. + if (not isinstance(response, Exception)) and response.needsSigning(): + if not self.signatory: + raise ValueError( + "Must have a store to sign this request: %s" % + (response,), response) + if response.fields.hasKey(OPENID_NS, 'sig'): + raise AlreadySigned(response) + response = self.signatory.sign(response) + return super(SigningEncoder, self).encode(response) + + + +class Decoder(object): + """I decode an incoming web request in to a L{OpenIDRequest}. + """ + + _handlers = { + 'checkid_setup': CheckIDRequest.fromMessage, + 'checkid_immediate': CheckIDRequest.fromMessage, + 'check_authentication': CheckAuthRequest.fromMessage, + 'associate': AssociateRequest.fromMessage, + } + + def __init__(self, server): + """Construct a Decoder. + + @param server: The server which I am decoding requests for. + (Necessary because some replies reference their server.) + @type server: L{Server} + """ + self.server = server + + def decode(self, query): + """I transform query parameters into an L{OpenIDRequest}. + + If the query does not seem to be an OpenID request at all, I return + C{None}. + + @param query: The query parameters as a dictionary with each + key mapping to one value. + @type query: dict + + @raises ProtocolError: When the query does not seem to be a valid + OpenID request. + + @returntype: L{OpenIDRequest} + """ + if not query: + return None + + try: + message = Message.fromPostArgs(query) + except InvalidOpenIDNamespace, err: + # It's useful to have a Message attached to a ProtocolError, so we + # override the bad ns value to build a Message out of it. Kinda + # kludgy, since it's made of lies, but the parts that aren't lies + # are more useful than a 'None'. + query = query.copy() + query['openid.ns'] = OPENID2_NS + message = Message.fromPostArgs(query) + raise ProtocolError(message, str(err)) + + mode = message.getArg(OPENID_NS, 'mode') + if not mode: + fmt = "No mode value in message %s" + raise ProtocolError(message, text=fmt % (message,)) + + handler = self._handlers.get(mode, self.defaultDecoder) + return handler(message, self.server.op_endpoint) + + + def defaultDecoder(self, message, server): + """Called to decode queries when no handler for that mode is found. + + @raises ProtocolError: This implementation always raises + L{ProtocolError}. + """ + mode = message.getArg(OPENID_NS, 'mode') + fmt = "Unrecognized OpenID mode %r" + raise ProtocolError(message, text=fmt % (mode,)) + + + +class Server(object): + """I handle requests for an OpenID server. + + Some types of requests (those which are not C{checkid} requests) may be + handed to my L{handleRequest} method, and I will take care of it and + return a response. + + For your convenience, I also provide an interface to L{Decoder.decode} + and L{SigningEncoder.encode} through my methods L{decodeRequest} and + L{encodeResponse}. + + All my state is encapsulated in an + L{OpenIDStore}, which means + I'm not generally pickleable but I am easy to reconstruct. + + Example:: + + oserver = Server(FileOpenIDStore(data_path), "http://example.com/op") + request = oserver.decodeRequest(query) + if request.mode in ['checkid_immediate', 'checkid_setup']: + if self.isAuthorized(request.identity, request.trust_root): + response = request.answer(True) + elif request.immediate: + response = request.answer(False) + else: + self.showDecidePage(request) + return + else: + response = oserver.handleRequest(request) + + webresponse = oserver.encode(response) + + @ivar signatory: I'm using this for associate requests and to sign things. + @type signatory: L{Signatory} + + @ivar decoder: I'm using this to decode things. + @type decoder: L{Decoder} + + @ivar encoder: I'm using this to encode things. + @type encoder: L{Encoder} + + @ivar op_endpoint: My URL. + @type op_endpoint: str + + @ivar negotiator: I use this to determine which kinds of + associations I can make and how. + @type negotiator: L{openid.association.SessionNegotiator} + """ + + signatoryClass = Signatory + encoderClass = SigningEncoder + decoderClass = Decoder + + def __init__(self, store, op_endpoint=None): + """A new L{Server}. + + @param store: The back-end where my associations are stored. + @type store: L{openid.store.interface.OpenIDStore} + + @param op_endpoint: My URL, the fully qualified address of this + server's endpoint, i.e. C{http://example.com/server} + @type op_endpoint: str + + @change: C{op_endpoint} is new in library version 2.0. It + currently defaults to C{None} for compatibility with + earlier versions of the library, but you must provide it + if you want to respond to any version 2 OpenID requests. + """ + self.store = store + self.signatory = self.signatoryClass(self.store) + self.encoder = self.encoderClass(self.signatory) + self.decoder = self.decoderClass(self) + self.negotiator = default_negotiator.copy() + + if not op_endpoint: + warnings.warn("%s.%s constructor requires op_endpoint parameter " + "for OpenID 2.0 servers" % + (self.__class__.__module__, self.__class__.__name__), + stacklevel=2) + self.op_endpoint = op_endpoint + + + def handleRequest(self, request): + """Handle a request. + + Give me a request, I will give you a response. Unless it's a type + of request I cannot handle myself, in which case I will raise + C{NotImplementedError}. In that case, you can handle it yourself, + or add a method to me for handling that request type. + + @raises NotImplementedError: When I do not have a handler defined + for that type of request. + + @returntype: L{OpenIDResponse} + """ + handler = getattr(self, 'openid_' + request.mode, None) + if handler is not None: + return handler(request) + else: + raise NotImplementedError( + "%s has no handler for a request of mode %r." % + (self, request.mode)) + + + def openid_check_authentication(self, request): + """Handle and respond to C{check_authentication} requests. + + @returntype: L{OpenIDResponse} + """ + return request.answer(self.signatory) + + + def openid_associate(self, request): + """Handle and respond to C{associate} requests. + + @returntype: L{OpenIDResponse} + """ + # XXX: TESTME + assoc_type = request.assoc_type + session_type = request.session.session_type + if self.negotiator.isAllowed(assoc_type, session_type): + assoc = self.signatory.createAssociation(dumb=False, + assoc_type=assoc_type) + return request.answer(assoc) + else: + message = ('Association type %r is not supported with ' + 'session type %r' % (assoc_type, session_type)) + (preferred_assoc_type, preferred_session_type) = \ + self.negotiator.getAllowedType() + return request.answerUnsupported( + message, + preferred_assoc_type, + preferred_session_type) + + + def decodeRequest(self, query): + """Transform query parameters into an L{OpenIDRequest}. + + If the query does not seem to be an OpenID request at all, I return + C{None}. + + @param query: The query parameters as a dictionary with each + key mapping to one value. + @type query: dict + + @raises ProtocolError: When the query does not seem to be a valid + OpenID request. + + @returntype: L{OpenIDRequest} + + @see: L{Decoder.decode} + """ + return self.decoder.decode(query) + + + def encodeResponse(self, response): + """Encode a response to a L{WebResponse}, signing it first if appropriate. + + @raises EncodingError: When I can't figure out how to encode this + message. + + @raises AlreadySigned: When this response is already signed. + + @returntype: L{WebResponse} + + @see: L{SigningEncoder.encode} + """ + return self.encoder.encode(response) + + + +class ProtocolError(Exception): + """A message did not conform to the OpenID protocol. + + @ivar message: The query that is failing to be a valid OpenID request. + @type message: openid.message.Message + """ + + def __init__(self, message, text=None, reference=None, contact=None): + """When an error occurs. + + @param message: The message that is failing to be a valid + OpenID request. + @type message: openid.message.Message + + @param text: A message about the encountered error. Set as C{args[0]}. + @type text: str + """ + self.openid_message = message + self.reference = reference + self.contact = contact + assert type(message) not in [str, unicode] + Exception.__init__(self, text) + + + def getReturnTo(self): + """Get the return_to argument from the request, if any. + + @returntype: str + """ + if self.openid_message is None: + return None + else: + return self.openid_message.getArg(OPENID_NS, 'return_to') + + def hasReturnTo(self): + """Did this request have a return_to parameter? + + @returntype: bool + """ + return self.getReturnTo() is not None + + def toMessage(self): + """Generate a Message object for sending to the relying party, + after encoding. + """ + namespace = self.openid_message.getOpenIDNamespace() + reply = Message(namespace) + reply.setArg(OPENID_NS, 'mode', 'error') + reply.setArg(OPENID_NS, 'error', str(self)) + + if self.contact is not None: + reply.setArg(OPENID_NS, 'contact', str(self.contact)) + + if self.reference is not None: + reply.setArg(OPENID_NS, 'reference', str(self.reference)) + + return reply + + # implements IEncodable + + def encodeToURL(self): + return self.toMessage().toURL(self.getReturnTo()) + + def encodeToKVForm(self): + return self.toMessage().toKVForm() + + def toFormMarkup(self): + """Encode to HTML form markup for POST. + + @since: 2.1.0 + """ + return self.toMessage().toFormMarkup(self.getReturnTo()) + + def toHTML(self): + """Encode to a full HTML page, wrapping the form markup in a page + that will autosubmit the form. + + @since: 2.1.? + """ + return oidutil.autoSubmitHTML(self.toFormMarkup()) + + def whichEncoding(self): + """How should I be encoded? + + @returns: one of ENCODE_URL, ENCODE_KVFORM, or None. If None, + I cannot be encoded as a protocol message and should be + displayed to the user. + """ + if self.hasReturnTo(): + if self.openid_message.getOpenIDNamespace() == OPENID2_NS and \ + len(self.encodeToURL()) > OPENID1_URL_LIMIT: + return ENCODE_HTML_FORM + else: + return ENCODE_URL + + if self.openid_message is None: + return None + + mode = self.openid_message.getArg(OPENID_NS, 'mode') + if mode: + if mode not in BROWSER_REQUEST_MODES: + return ENCODE_KVFORM + + # According to the OpenID spec as of this writing, we are probably + # supposed to switch on request type here (GET versus POST) to figure + # out if we're supposed to print machine-readable or human-readable + # content at this point. GET/POST seems like a pretty lousy way of + # making the distinction though, as it's just as possible that the + # user agent could have mistakenly been directed to post to the + # server URL. + + # Basically, if your request was so broken that you didn't manage to + # include an openid.mode, I'm not going to worry too much about + # returning you something you can't parse. + return None + + + +class VersionError(Exception): + """Raised when an operation was attempted that is not compatible with + the protocol version being used.""" + + + +class NoReturnToError(Exception): + """Raised when a response to a request cannot be generated because + the request contains no return_to URL. + """ + pass + + + +class EncodingError(Exception): + """Could not encode this as a protocol message. + + You should probably render it and show it to the user. + + @ivar response: The response that failed to encode. + @type response: L{OpenIDResponse} + """ + + def __init__(self, response, explanation=None): + Exception.__init__(self, response) + self.response = response + self.explanation = explanation + + def __str__(self): + if self.explanation: + s = '%s: %s' % (self.__class__.__name__, + self.explanation) + else: + s = '%s for Response %s' % ( + self.__class__.__name__, self.response) + return s + + +class AlreadySigned(EncodingError): + """This response is already signed.""" + + + +class UntrustedReturnURL(ProtocolError): + """A return_to is outside the trust_root.""" + + def __init__(self, message, return_to, trust_root): + ProtocolError.__init__(self, message) + self.return_to = return_to + self.trust_root = trust_root + + def __str__(self): + return "return_to %r not under trust_root %r" % (self.return_to, + self.trust_root) + + +class MalformedReturnURL(ProtocolError): + """The return_to URL doesn't look like a valid URL.""" + def __init__(self, openid_message, return_to): + self.return_to = return_to + ProtocolError.__init__(self, openid_message) + + + +class MalformedTrustRoot(ProtocolError): + """The trust root is not well-formed. + + @see: OpenID Specs, U{openid.trust_root} + """ + pass + + +#class IEncodable: # Interface +# def encodeToURL(return_to): +# """Encode a response as a URL for redirection. +# +# @returns: A URL to direct the user agent back to. +# @returntype: str +# """ +# pass +# +# def encodeToKvform(): +# """Encode a response in key-value colon/newline format. +# +# This is a machine-readable format used to respond to messages which +# came directly from the consumer and not through the user agent. +# +# @see: OpenID Specs, +# U{Key-Value Colon/Newline format} +# +# @returntype: str +# """ +# pass +# +# def whichEncoding(): +# """How should I be encoded? +# +# @returns: one of ENCODE_URL, ENCODE_KVFORM, or None. If None, +# I cannot be encoded as a protocol message and should be +# displayed to the user. +# """ +# pass diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/server/trustroot.py b/desktop/core/ext-py/python-openid-2.2.5/openid/server/trustroot.py new file mode 100644 index 0000000..84a100e --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/server/trustroot.py @@ -0,0 +1,454 @@ +# -*- test-case-name: openid.test.test_rpverify -*- +""" +This module contains the C{L{TrustRoot}} class, which helps handle +trust root checking. This module is used by the +C{L{openid.server.server}} module, but it is also available to server +implementers who wish to use it for additional trust root checking. + +It also implements relying party return_to URL verification, based on +the realm. +""" + +__all__ = [ + 'TrustRoot', + 'RP_RETURN_TO_URL_TYPE', + 'extractReturnToURLs', + 'returnToMatches', + 'verifyReturnTo', + ] + +from openid import oidutil +from openid import urinorm +from openid.yadis import services + +from urlparse import urlparse, urlunparse +import re + +############################################ +_protocols = ['http', 'https'] +_top_level_domains = [ + 'ac', 'ad', 'ae', 'aero', 'af', 'ag', 'ai', 'al', 'am', 'an', + 'ao', 'aq', 'ar', 'arpa', 'as', 'asia', 'at', 'au', 'aw', + 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh', 'bi', + 'biz', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv', 'bw', + 'by', 'bz', 'ca', 'cat', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', + 'ck', 'cl', 'cm', 'cn', 'co', 'com', 'coop', 'cr', 'cu', 'cv', + 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', + 'edu', 'ee', 'eg', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', + 'fm', 'fo', 'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', + 'gi', 'gl', 'gm', 'gn', 'gov', 'gp', 'gq', 'gr', 'gs', 'gt', + 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu', 'id', + 'ie', 'il', 'im', 'in', 'info', 'int', 'io', 'iq', 'ir', 'is', + 'it', 'je', 'jm', 'jo', 'jobs', 'jp', 'ke', 'kg', 'kh', 'ki', + 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', + 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', + 'md', 'me', 'mg', 'mh', 'mil', 'mk', 'ml', 'mm', 'mn', 'mo', + 'mobi', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'museum', 'mv', + 'mw', 'mx', 'my', 'mz', 'na', 'name', 'nc', 'ne', 'net', 'nf', + 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 'org', + 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', + 'pro', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', + 'rw', 'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', + 'sk', 'sl', 'sm', 'sn', 'so', 'sr', 'st', 'su', 'sv', 'sy', + 'sz', 'tc', 'td', 'tel', 'tf', 'tg', 'th', 'tj', 'tk', 'tl', + 'tm', 'tn', 'to', 'tp', 'tr', 'travel', 'tt', 'tv', 'tw', + 'tz', 'ua', 'ug', 'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', + 'vg', 'vi', 'vn', 'vu', 'wf', 'ws', 'xn--0zwm56d', + 'xn--11b5bs3a9aj6g', 'xn--80akhbyknj4f', 'xn--9t4b11yi5a', + 'xn--deba0ad', 'xn--g6w251d', 'xn--hgbk6aj7f53bba', + 'xn--hlcj6aya9esc7a', 'xn--jxalpdlp', 'xn--kgbechtv', + 'xn--zckzah', 'ye', 'yt', 'yu', 'za', 'zm', 'zw'] + +# Build from RFC3986, section 3.2.2. Used to reject hosts with invalid +# characters. +host_segment_re = re.compile( + r"(?:[-a-zA-Z0-9!$&'\(\)\*+,;=._~]|%[a-zA-Z0-9]{2})+$") + +class RealmVerificationRedirected(Exception): + """Attempting to verify this realm resulted in a redirect. + + @since: 2.1.0 + """ + def __init__(self, relying_party_url, rp_url_after_redirects): + self.relying_party_url = relying_party_url + self.rp_url_after_redirects = rp_url_after_redirects + + def __str__(self): + return ("Attempting to verify %r resulted in " + "redirect to %r" % + (self.relying_party_url, + self.rp_url_after_redirects)) + + +def _parseURL(url): + try: + url = urinorm.urinorm(url) + except ValueError: + return None + proto, netloc, path, params, query, frag = urlparse(url) + if not path: + # Python <2.4 does not parse URLs with no path properly + if not query and '?' in netloc: + netloc, query = netloc.split('?', 1) + + path = '/' + + path = urlunparse(('', '', path, params, query, frag)) + + if ':' in netloc: + try: + host, port = netloc.split(':') + except ValueError: + return None + + if not re.match(r'\d+$', port): + return None + else: + host = netloc + port = '' + + host = host.lower() + if not host_segment_re.match(host): + return None + + return proto, host, port, path + +class TrustRoot(object): + """ + This class represents an OpenID trust root. The C{L{parse}} + classmethod accepts a trust root string, producing a + C{L{TrustRoot}} object. The method OpenID server implementers + would be most likely to use is the C{L{isSane}} method, which + checks the trust root for given patterns that indicate that the + trust root is too broad or points to a local network resource. + + @sort: parse, isSane + """ + + def __init__(self, unparsed, proto, wildcard, host, port, path): + self.unparsed = unparsed + self.proto = proto + self.wildcard = wildcard + self.host = host + self.port = port + self.path = path + + def isSane(self): + """ + This method checks the to see if a trust root represents a + reasonable (sane) set of URLs. 'http://*.com/', for example + is not a reasonable pattern, as it cannot meaningfully specify + the site claiming it. This function attempts to find many + related examples, but it can only work via heuristics. + Negative responses from this method should be treated as + advisory, used only to alert the user to examine the trust + root carefully. + + + @return: Whether the trust root is sane + + @rtype: C{bool} + """ + + if self.host == 'localhost': + return True + + host_parts = self.host.split('.') + if self.wildcard: + assert host_parts[0] == '', host_parts + del host_parts[0] + + # If it's an absolute domain name, remove the empty string + # from the end. + if host_parts and not host_parts[-1]: + del host_parts[-1] + + if not host_parts: + return False + + # Do not allow adjacent dots + if '' in host_parts: + return False + + tld = host_parts[-1] + if tld not in _top_level_domains: + return False + + if len(host_parts) == 1: + return False + + if self.wildcard: + if len(tld) == 2 and len(host_parts[-2]) <= 3: + # It's a 2-letter tld with a short second to last segment + # so there needs to be more than two segments specified + # (e.g. *.co.uk is insane) + return len(host_parts) > 2 + + # Passed all tests for insanity. + return True + + def validateURL(self, url): + """ + Validates a URL against this trust root. + + + @param url: The URL to check + + @type url: C{str} + + + @return: Whether the given URL is within this trust root. + + @rtype: C{bool} + """ + + url_parts = _parseURL(url) + if url_parts is None: + return False + + proto, host, port, path = url_parts + + if proto != self.proto: + return False + + if port != self.port: + return False + + if '*' in host: + return False + + if not self.wildcard: + if host != self.host: + return False + elif ((not host.endswith(self.host)) and + ('.' + host) != self.host): + return False + + if path != self.path: + path_len = len(self.path) + trust_prefix = self.path[:path_len] + url_prefix = path[:path_len] + + # must be equal up to the length of the path, at least + if trust_prefix != url_prefix: + return False + + # These characters must be on the boundary between the end + # of the trust root's path and the start of the URL's + # path. + if '?' in self.path: + allowed = '&' + else: + allowed = '?/' + + return (self.path[-1] in allowed or + path[path_len] in allowed) + + return True + + def parse(cls, trust_root): + """ + This method creates a C{L{TrustRoot}} instance from the given + input, if possible. + + + @param trust_root: This is the trust root to parse into a + C{L{TrustRoot}} object. + + @type trust_root: C{str} + + + @return: A C{L{TrustRoot}} instance if trust_root parses as a + trust root, C{None} otherwise. + + @rtype: C{NoneType} or C{L{TrustRoot}} + """ + url_parts = _parseURL(trust_root) + if url_parts is None: + return None + + proto, host, port, path = url_parts + + # check for valid prototype + if proto not in _protocols: + return None + + # check for URI fragment + if path.find('#') != -1: + return None + + # extract wildcard if it is there + if host.find('*', 1) != -1: + # wildcard must be at start of domain: *.foo.com, not foo.*.com + return None + + if host.startswith('*'): + # Starts with star, so must have a dot after it (if a + # domain is specified) + if len(host) > 1 and host[1] != '.': + return None + + host = host[1:] + wilcard = True + else: + wilcard = False + + # we have a valid trust root + tr = cls(trust_root, proto, wilcard, host, port, path) + + return tr + + parse = classmethod(parse) + + def checkSanity(cls, trust_root_string): + """str -> bool + + is this a sane trust root? + """ + trust_root = cls.parse(trust_root_string) + if trust_root is None: + return False + else: + return trust_root.isSane() + + checkSanity = classmethod(checkSanity) + + def checkURL(cls, trust_root, url): + """quick func for validating a url against a trust root. See the + TrustRoot class if you need more control.""" + tr = cls.parse(trust_root) + return tr is not None and tr.validateURL(url) + + checkURL = classmethod(checkURL) + + def buildDiscoveryURL(self): + """Return a discovery URL for this realm. + + This function does not check to make sure that the realm is + valid. Its behaviour on invalid inputs is undefined. + + @rtype: str + + @returns: The URL upon which relying party discovery should be run + in order to verify the return_to URL + + @since: 2.1.0 + """ + if self.wildcard: + # Use "www." in place of the star + assert self.host.startswith('.'), self.host + www_domain = 'www' + self.host + return '%s://%s%s' % (self.proto, www_domain, self.path) + else: + return self.unparsed + + def __repr__(self): + return "TrustRoot(%r, %r, %r, %r, %r, %r)" % ( + self.unparsed, self.proto, self.wildcard, self.host, self.port, + self.path) + + def __str__(self): + return repr(self) + +# The URI for relying party discovery, used in realm verification. +# +# XXX: This should probably live somewhere else (like in +# openid.consumer or openid.yadis somewhere) +RP_RETURN_TO_URL_TYPE = 'http://specs.openid.net/auth/2.0/return_to' + +def _extractReturnURL(endpoint): + """If the endpoint is a relying party OpenID return_to endpoint, + return the endpoint URL. Otherwise, return None. + + This function is intended to be used as a filter for the Yadis + filtering interface. + + @see: C{L{openid.yadis.services}} + @see: C{L{openid.yadis.filters}} + + @param endpoint: An XRDS BasicServiceEndpoint, as returned by + performing Yadis dicovery. + + @returns: The endpoint URL or None if the endpoint is not a + relying party endpoint. + @rtype: str or NoneType + """ + if endpoint.matchTypes([RP_RETURN_TO_URL_TYPE]): + return endpoint.uri + else: + return None + +def returnToMatches(allowed_return_to_urls, return_to): + """Is the return_to URL under one of the supplied allowed + return_to URLs? + + @since: 2.1.0 + """ + + for allowed_return_to in allowed_return_to_urls: + # A return_to pattern works the same as a realm, except that + # it's not allowed to use a wildcard. We'll model this by + # parsing it as a realm, and not trying to match it if it has + # a wildcard. + + return_realm = TrustRoot.parse(allowed_return_to) + if (# Parses as a trust root + return_realm is not None and + + # Does not have a wildcard + not return_realm.wildcard and + + # Matches the return_to that we passed in with it + return_realm.validateURL(return_to) + ): + return True + + # No URL in the list matched + return False + +def getAllowedReturnURLs(relying_party_url): + """Given a relying party discovery URL return a list of return_to URLs. + + @since: 2.1.0 + """ + (rp_url_after_redirects, return_to_urls) = services.getServiceEndpoints( + relying_party_url, _extractReturnURL) + + if rp_url_after_redirects != relying_party_url: + # Verification caused a redirect + raise RealmVerificationRedirected( + relying_party_url, rp_url_after_redirects) + + return return_to_urls + +# _vrfy parameter is there to make testing easier +def verifyReturnTo(realm_str, return_to, _vrfy=getAllowedReturnURLs): + """Verify that a return_to URL is valid for the given realm. + + This function builds a discovery URL, performs Yadis discovery on + it, makes sure that the URL does not redirect, parses out the + return_to URLs, and finally checks to see if the current return_to + URL matches the return_to. + + @raises DiscoveryFailure: When Yadis discovery fails + @returns: True if the return_to URL is valid for the realm + + @since: 2.1.0 + """ + realm = TrustRoot.parse(realm_str) + if realm is None: + # The realm does not parse as a URL pattern + return False + + try: + allowable_urls = _vrfy(realm.buildDiscoveryURL()) + except RealmVerificationRedirected, err: + oidutil.log(str(err)) + return False + + if returnToMatches(allowable_urls, return_to): + return True + else: + oidutil.log("Failed to validate return_to %r for realm %r, was not " + "in %s" % (return_to, realm_str, allowable_urls)) + return False diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/sreg.py b/desktop/core/ext-py/python-openid-2.2.5/openid/sreg.py new file mode 100644 index 0000000..d665a5d --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/sreg.py @@ -0,0 +1,7 @@ +"""moved to L{openid.extensions.sreg}""" + +import warnings +warnings.warn("openid.sreg has moved to openid.extensions.sreg", + DeprecationWarning) + +from openid.extensions.sreg import * diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/store/__init__.py b/desktop/core/ext-py/python-openid-2.2.5/openid/store/__init__.py new file mode 100644 index 0000000..76509b5 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/store/__init__.py @@ -0,0 +1,8 @@ +""" +This package contains the modules related to this library's use of +persistent storage. + +@sort: interface, filestore, sqlstore, memstore +""" + +__all__ = ['interface', 'filestore', 'sqlstore', 'memstore', 'nonce'] diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/store/filestore.py b/desktop/core/ext-py/python-openid-2.2.5/openid/store/filestore.py new file mode 100644 index 0000000..ced3cee --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/store/filestore.py @@ -0,0 +1,426 @@ +""" +This module contains an C{L{OpenIDStore}} implementation backed by +flat files. +""" + +import string +import os +import os.path +import time + +from errno import EEXIST, ENOENT + +try: + from tempfile import mkstemp +except ImportError: + # Python < 2.3 + import warnings + warnings.filterwarnings("ignore", + "tempnam is a potential security risk", + RuntimeWarning, + "openid.store.filestore") + + def mkstemp(dir): + for _ in range(5): + name = os.tempnam(dir) + try: + fd = os.open(name, os.O_CREAT | os.O_EXCL | os.O_RDWR, 0600) + except OSError, why: + if why.errno != EEXIST: + raise + else: + return fd, name + + raise RuntimeError('Failed to get temp file after 5 attempts') + +from openid.association import Association +from openid.store.interface import OpenIDStore +from openid.store import nonce +from openid import cryptutil, oidutil + +_filename_allowed = string.ascii_letters + string.digits + '.' +try: + # 2.4 + set +except NameError: + try: + # 2.3 + import sets + except ImportError: + # Python < 2.2 + d = {} + for c in _filename_allowed: + d[c] = None + _isFilenameSafe = d.has_key + del d + else: + _isFilenameSafe = sets.Set(_filename_allowed).__contains__ +else: + _isFilenameSafe = set(_filename_allowed).__contains__ + +def _safe64(s): + h64 = oidutil.toBase64(cryptutil.sha1(s)) + h64 = h64.replace('+', '_') + h64 = h64.replace('/', '.') + h64 = h64.replace('=', '') + return h64 + +def _filenameEscape(s): + filename_chunks = [] + for c in s: + if _isFilenameSafe(c): + filename_chunks.append(c) + else: + filename_chunks.append('_%02X' % ord(c)) + return ''.join(filename_chunks) + +def _removeIfPresent(filename): + """Attempt to remove a file, returning whether the file existed at + the time of the call. + + str -> bool + """ + try: + os.unlink(filename) + except OSError, why: + if why.errno == ENOENT: + # Someone beat us to it, but it's gone, so that's OK + return 0 + else: + raise + else: + # File was present + return 1 + +def _ensureDir(dir_name): + """Create dir_name as a directory if it does not exist. If it + exists, make sure that it is, in fact, a directory. + + Can raise OSError + + str -> NoneType + """ + try: + os.makedirs(dir_name) + except OSError, why: + if why.errno != EEXIST or not os.path.isdir(dir_name): + raise + +class FileOpenIDStore(OpenIDStore): + """ + This is a filesystem-based store for OpenID associations and + nonces. This store should be safe for use in concurrent systems + on both windows and unix (excluding NFS filesystems). There are a + couple race conditions in the system, but those failure cases have + been set up in such a way that the worst-case behavior is someone + having to try to log in a second time. + + Most of the methods of this class are implementation details. + People wishing to just use this store need only pay attention to + the C{L{__init__}} method. + + Methods of this object can raise OSError if unexpected filesystem + conditions, such as bad permissions or missing directories, occur. + """ + + def __init__(self, directory): + """ + Initializes a new FileOpenIDStore. This initializes the + nonce and association directories, which are subdirectories of + the directory passed in. + + @param directory: This is the directory to put the store + directories in. + + @type directory: C{str} + """ + # Make absolute + directory = os.path.normpath(os.path.abspath(directory)) + + self.nonce_dir = os.path.join(directory, 'nonces') + + self.association_dir = os.path.join(directory, 'associations') + + # Temp dir must be on the same filesystem as the assciations + # directory + self.temp_dir = os.path.join(directory, 'temp') + + self.max_nonce_age = 6 * 60 * 60 # Six hours, in seconds + + self._setup() + + def _setup(self): + """Make sure that the directories in which we store our data + exist. + + () -> NoneType + """ + _ensureDir(self.nonce_dir) + _ensureDir(self.association_dir) + _ensureDir(self.temp_dir) + + def _mktemp(self): + """Create a temporary file on the same filesystem as + self.association_dir. + + The temporary directory should not be cleaned if there are any + processes using the store. If there is no active process using + the store, it is safe to remove all of the files in the + temporary directory. + + () -> (file, str) + """ + fd, name = mkstemp(dir=self.temp_dir) + try: + file_obj = os.fdopen(fd, 'wb') + return file_obj, name + except: + _removeIfPresent(name) + raise + + def getAssociationFilename(self, server_url, handle): + """Create a unique filename for a given server url and + handle. This implementation does not assume anything about the + format of the handle. The filename that is returned will + contain the domain name from the server URL for ease of human + inspection of the data directory. + + (str, str) -> str + """ + if server_url.find('://') == -1: + raise ValueError('Bad server URL: %r' % server_url) + + proto, rest = server_url.split('://', 1) + domain = _filenameEscape(rest.split('/', 1)[0]) + url_hash = _safe64(server_url) + if handle: + handle_hash = _safe64(handle) + else: + handle_hash = '' + + filename = '%s-%s-%s-%s' % (proto, domain, url_hash, handle_hash) + + return os.path.join(self.association_dir, filename) + + def storeAssociation(self, server_url, association): + """Store an association in the association directory. + + (str, Association) -> NoneType + """ + association_s = association.serialize() + filename = self.getAssociationFilename(server_url, association.handle) + tmp_file, tmp = self._mktemp() + + try: + try: + tmp_file.write(association_s) + os.fsync(tmp_file.fileno()) + finally: + tmp_file.close() + + try: + os.rename(tmp, filename) + except OSError, why: + if why.errno != EEXIST: + raise + + # We only expect EEXIST to happen only on Windows. It's + # possible that we will succeed in unlinking the existing + # file, but not in putting the temporary file in place. + try: + os.unlink(filename) + except OSError, why: + if why.errno == ENOENT: + pass + else: + raise + + # Now the target should not exist. Try renaming again, + # giving up if it fails. + os.rename(tmp, filename) + except: + # If there was an error, don't leave the temporary file + # around. + _removeIfPresent(tmp) + raise + + def getAssociation(self, server_url, handle=None): + """Retrieve an association. If no handle is specified, return + the association with the latest expiration. + + (str, str or NoneType) -> Association or NoneType + """ + if handle is None: + handle = '' + + # The filename with the empty handle is a prefix of all other + # associations for the given server URL. + filename = self.getAssociationFilename(server_url, handle) + + if handle: + return self._getAssociation(filename) + else: + association_files = os.listdir(self.association_dir) + matching_files = [] + # strip off the path to do the comparison + name = os.path.basename(filename) + for association_file in association_files: + if association_file.startswith(name): + matching_files.append(association_file) + + matching_associations = [] + # read the matching files and sort by time issued + for name in matching_files: + full_name = os.path.join(self.association_dir, name) + association = self._getAssociation(full_name) + if association is not None: + matching_associations.append( + (association.issued, association)) + + matching_associations.sort() + + # return the most recently issued one. + if matching_associations: + (_, assoc) = matching_associations[-1] + return assoc + else: + return None + + def _getAssociation(self, filename): + try: + assoc_file = file(filename, 'rb') + except IOError, why: + if why.errno == ENOENT: + # No association exists for that URL and handle + return None + else: + raise + else: + try: + assoc_s = assoc_file.read() + finally: + assoc_file.close() + + try: + association = Association.deserialize(assoc_s) + except ValueError: + _removeIfPresent(filename) + return None + + # Clean up expired associations + if association.getExpiresIn() == 0: + _removeIfPresent(filename) + return None + else: + return association + + def removeAssociation(self, server_url, handle): + """Remove an association if it exists. Do nothing if it does not. + + (str, str) -> bool + """ + assoc = self.getAssociation(server_url, handle) + if assoc is None: + return 0 + else: + filename = self.getAssociationFilename(server_url, handle) + return _removeIfPresent(filename) + + def useNonce(self, server_url, timestamp, salt): + """Return whether this nonce is valid. + + str -> bool + """ + if abs(timestamp - time.time()) > nonce.SKEW: + return False + + if server_url: + proto, rest = server_url.split('://', 1) + else: + # Create empty proto / rest values for empty server_url, + # which is part of a consumer-generated nonce. + proto, rest = '', '' + + domain = _filenameEscape(rest.split('/', 1)[0]) + url_hash = _safe64(server_url) + salt_hash = _safe64(salt) + + filename = '%08x-%s-%s-%s-%s' % (timestamp, proto, domain, + url_hash, salt_hash) + + filename = os.path.join(self.nonce_dir, filename) + try: + fd = os.open(filename, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0200) + except OSError, why: + if why.errno == EEXIST: + return False + else: + raise + else: + os.close(fd) + return True + + def _allAssocs(self): + all_associations = [] + + association_filenames = map( + lambda filename: os.path.join(self.association_dir, filename), + os.listdir(self.association_dir)) + for association_filename in association_filenames: + try: + association_file = file(association_filename, 'rb') + except IOError, why: + if why.errno == ENOENT: + oidutil.log("%s disappeared during %s._allAssocs" % ( + association_filename, self.__class__.__name__)) + else: + raise + else: + try: + assoc_s = association_file.read() + finally: + association_file.close() + + # Remove expired or corrupted associations + try: + association = Association.deserialize(assoc_s) + except ValueError: + _removeIfPresent(association_filename) + else: + all_associations.append( + (association_filename, association)) + + return all_associations + + def cleanup(self): + """Remove expired entries from the database. This is + potentially expensive, so only run when it is acceptable to + take time. + + () -> NoneType + """ + self.cleanupAssociations() + self.cleanupNonces() + + def cleanupAssociations(self): + removed = 0 + for assoc_filename, assoc in self._allAssocs(): + if assoc.getExpiresIn() == 0: + _removeIfPresent(assoc_filename) + removed += 1 + return removed + + def cleanupNonces(self): + nonces = os.listdir(self.nonce_dir) + now = time.time() + + removed = 0 + # Check all nonces for expiry + for nonce_fname in nonces: + timestamp = nonce_fname.split('-', 1)[0] + timestamp = int(timestamp, 16) + if abs(timestamp - now) > nonce.SKEW: + filename = os.path.join(self.nonce_dir, nonce_fname) + _removeIfPresent(filename) + removed += 1 + return removed diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/store/interface.py b/desktop/core/ext-py/python-openid-2.2.5/openid/store/interface.py new file mode 100644 index 0000000..bb90972 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/store/interface.py @@ -0,0 +1,197 @@ +""" +This module contains the definition of the C{L{OpenIDStore}} +interface. +""" + +class OpenIDStore(object): + """ + This is the interface for the store objects the OpenID library + uses. It is a single class that provides all of the persistence + mechanisms that the OpenID library needs, for both servers and + consumers. + + @change: Version 2.0 removed the C{storeNonce}, C{getAuthKey}, and C{isDumb} + methods, and changed the behavior of the C{L{useNonce}} method + to support one-way nonces. It added C{L{cleanupNonces}}, + C{L{cleanupAssociations}}, and C{L{cleanup}}. + + @sort: storeAssociation, getAssociation, removeAssociation, + useNonce + """ + + def storeAssociation(self, server_url, association): + """ + This method puts a C{L{Association + }} object into storage, + retrievable by server URL and handle. + + + @param server_url: The URL of the identity server that this + association is with. Because of the way the server + portion of the library uses this interface, don't assume + there are any limitations on the character set of the + input string. In particular, expect to see unescaped + non-url-safe characters in the server_url field. + + @type server_url: C{str} + + + @param association: The C{L{Association + }} to store. + + @type association: C{L{Association + }} + + + @return: C{None} + + @rtype: C{NoneType} + """ + raise NotImplementedError + + def getAssociation(self, server_url, handle=None): + """ + This method returns an C{L{Association + }} object from storage that + matches the server URL and, if specified, handle. It returns + C{None} if no such association is found or if the matching + association is expired. + + If no handle is specified, the store may return any + association which matches the server URL. If multiple + associations are valid, the recommended return value for this + method is the one most recently issued. + + This method is allowed (and encouraged) to garbage collect + expired associations when found. This method must not return + expired associations. + + + @param server_url: The URL of the identity server to get the + association for. Because of the way the server portion of + the library uses this interface, don't assume there are + any limitations on the character set of the input string. + In particular, expect to see unescaped non-url-safe + characters in the server_url field. + + @type server_url: C{str} + + + @param handle: This optional parameter is the handle of the + specific association to get. If no specific handle is + provided, any valid association matching the server URL is + returned. + + @type handle: C{str} or C{NoneType} + + + @return: The C{L{Association + }} for the given identity + server. + + @rtype: C{L{Association }} or + C{NoneType} + """ + raise NotImplementedError + + def removeAssociation(self, server_url, handle): + """ + This method removes the matching association if it's found, + and returns whether the association was removed or not. + + + @param server_url: The URL of the identity server the + association to remove belongs to. Because of the way the + server portion of the library uses this interface, don't + assume there are any limitations on the character set of + the input string. In particular, expect to see unescaped + non-url-safe characters in the server_url field. + + @type server_url: C{str} + + + @param handle: This is the handle of the association to + remove. If there isn't an association found that matches + both the given URL and handle, then there was no matching + handle found. + + @type handle: C{str} + + + @return: Returns whether or not the given association existed. + + @rtype: C{bool} or C{int} + """ + raise NotImplementedError + + def useNonce(self, server_url, timestamp, salt): + """Called when using a nonce. + + This method should return C{True} if the nonce has not been + used before, and store it for a while to make sure nobody + tries to use the same value again. If the nonce has already + been used or the timestamp is not current, return C{False}. + + You may use L{openid.store.nonce.SKEW} for your timestamp window. + + @change: In earlier versions, round-trip nonces were used and + a nonce was only valid if it had been previously stored + with C{storeNonce}. Version 2.0 uses one-way nonces, + requiring a different implementation here that does not + depend on a C{storeNonce} call. (C{storeNonce} is no + longer part of the interface.) + + @param server_url: The URL of the server from which the nonce + originated. + + @type server_url: C{str} + + @param timestamp: The time that the nonce was created (to the + nearest second), in seconds since January 1 1970 UTC. + @type timestamp: C{int} + + @param salt: A random string that makes two nonces from the + same server issued during the same second unique. + @type salt: str + + @return: Whether or not the nonce was valid. + + @rtype: C{bool} + """ + raise NotImplementedError + + def cleanupNonces(self): + """Remove expired nonces from the store. + + Discards any nonce from storage that is old enough that its + timestamp would not pass L{useNonce}. + + This method is not called in the normal operation of the + library. It provides a way for store admins to keep + their storage from filling up with expired data. + + @return: the number of nonces expired. + @returntype: int + """ + raise NotImplementedError + + def cleanupAssociations(self): + """Remove expired associations from the store. + + This method is not called in the normal operation of the + library. It provides a way for store admins to keep + their storage from filling up with expired data. + + @return: the number of associations expired. + @returntype: int + """ + raise NotImplementedError + + def cleanup(self): + """Shortcut for C{L{cleanupNonces}()}, C{L{cleanupAssociations}()}. + + This method is not called in the normal operation of the + library. It provides a way for store admins to keep + their storage from filling up with expired data. + """ + return self.cleanupNonces(), self.cleanupAssociations() diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/store/memstore.py b/desktop/core/ext-py/python-openid-2.2.5/openid/store/memstore.py new file mode 100644 index 0000000..e2748fb --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/store/memstore.py @@ -0,0 +1,125 @@ +"""A simple store using only in-process memory.""" + +from openid.store import nonce + +import copy +import time + +class ServerAssocs(object): + def __init__(self): + self.assocs = {} + + def set(self, assoc): + self.assocs[assoc.handle] = assoc + + def get(self, handle): + return self.assocs.get(handle) + + def remove(self, handle): + try: + del self.assocs[handle] + except KeyError: + return False + else: + return True + + def best(self): + """Returns association with the oldest issued date. + + or None if there are no associations. + """ + best = None + for assoc in self.assocs.values(): + if best is None or best.issued < assoc.issued: + best = assoc + return best + + def cleanup(self): + """Remove expired associations. + + @return: tuple of (removed associations, remaining associations) + """ + remove = [] + for handle, assoc in self.assocs.iteritems(): + if assoc.getExpiresIn() == 0: + remove.append(handle) + for handle in remove: + del self.assocs[handle] + return len(remove), len(self.assocs) + + + +class MemoryStore(object): + """In-process memory store. + + Use for single long-running processes. No persistence supplied. + """ + def __init__(self): + self.server_assocs = {} + self.nonces = {} + + def _getServerAssocs(self, server_url): + try: + return self.server_assocs[server_url] + except KeyError: + assocs = self.server_assocs[server_url] = ServerAssocs() + return assocs + + def storeAssociation(self, server_url, assoc): + assocs = self._getServerAssocs(server_url) + assocs.set(copy.deepcopy(assoc)) + + def getAssociation(self, server_url, handle=None): + assocs = self._getServerAssocs(server_url) + if handle is None: + return assocs.best() + else: + return assocs.get(handle) + + def removeAssociation(self, server_url, handle): + assocs = self._getServerAssocs(server_url) + return assocs.remove(handle) + + def useNonce(self, server_url, timestamp, salt): + if abs(timestamp - time.time()) > nonce.SKEW: + return False + + anonce = (str(server_url), int(timestamp), str(salt)) + if anonce in self.nonces: + return False + else: + self.nonces[anonce] = None + return True + + def cleanupNonces(self): + now = time.time() + expired = [] + for anonce in self.nonces.iterkeys(): + if abs(anonce[1] - now) > nonce.SKEW: + # removing items while iterating over the set could be bad. + expired.append(anonce) + + for anonce in expired: + del self.nonces[anonce] + return len(expired) + + def cleanupAssociations(self): + remove_urls = [] + removed_assocs = 0 + for server_url, assocs in self.server_assocs.iteritems(): + removed, remaining = assocs.cleanup() + removed_assocs += removed + if not remaining: + remove_urls.append(server_url) + + # Remove entries from server_assocs that had none remaining. + for server_url in remove_urls: + del self.server_assocs[server_url] + return removed_assocs + + def __eq__(self, other): + return ((self.server_assocs == other.server_assocs) and + (self.nonces == other.nonces)) + + def __ne__(self, other): + return not (self == other) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/store/nonce.py b/desktop/core/ext-py/python-openid-2.2.5/openid/store/nonce.py new file mode 100644 index 0000000..e9337a8 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/store/nonce.py @@ -0,0 +1,98 @@ +__all__ = [ + 'split', + 'mkNonce', + 'checkTimestamp', + ] + +from openid import cryptutil +from time import strptime, strftime, gmtime, time +from calendar import timegm +import string + +NONCE_CHARS = string.ascii_letters + string.digits + +# Keep nonces for five hours (allow five hours for the combination of +# request time and clock skew). This is probably way more than is +# necessary, but there is not much overhead in storing nonces. +SKEW = 60 * 60 * 5 + +time_fmt = '%Y-%m-%dT%H:%M:%SZ' +time_str_len = len('0000-00-00T00:00:00Z') + +def split(nonce_string): + """Extract a timestamp from the given nonce string + + @param nonce_string: the nonce from which to extract the timestamp + @type nonce_string: str + + @returns: A pair of a Unix timestamp and the salt characters + @returntype: (int, str) + + @raises ValueError: if the nonce does not start with a correctly + formatted time string + """ + timestamp_str = nonce_string[:time_str_len] + try: + timestamp = timegm(strptime(timestamp_str, time_fmt)) + except AssertionError: # Python 2.2 + timestamp = -1 + if timestamp < 0: + raise ValueError('time out of range') + return timestamp, nonce_string[time_str_len:] + +def checkTimestamp(nonce_string, allowed_skew=SKEW, now=None): + """Is the timestamp that is part of the specified nonce string + within the allowed clock-skew of the current time? + + @param nonce_string: The nonce that is being checked + @type nonce_string: str + + @param allowed_skew: How many seconds should be allowed for + completing the request, allowing for clock skew. + @type allowed_skew: int + + @param now: The current time, as a Unix timestamp + @type now: int + + @returntype: bool + @returns: Whether the timestamp is correctly formatted and within + the allowed skew of the current time. + """ + try: + stamp, _ = split(nonce_string) + except ValueError: + return False + else: + if now is None: + now = time() + + # Time after which we should not use the nonce + past = now - allowed_skew + + # Time that is too far in the future for us to allow + future = now + allowed_skew + + # the stamp is not too far in the future and is not too far in + # the past + return past <= stamp <= future + +def mkNonce(when=None): + """Generate a nonce with the current timestamp + + @param when: Unix timestamp representing the issue time of the + nonce. Defaults to the current time. + @type when: int + + @returntype: str + @returns: A string that should be usable as a one-way nonce + + @see: time + """ + salt = cryptutil.randomString(6, NONCE_CHARS) + if when is None: + t = gmtime() + else: + t = gmtime(when) + + time_str = strftime(time_fmt, t) + return time_str + salt diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/store/sqlstore.py b/desktop/core/ext-py/python-openid-2.2.5/openid/store/sqlstore.py new file mode 100644 index 0000000..58c4337 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/store/sqlstore.py @@ -0,0 +1,516 @@ +""" +This module contains C{L{OpenIDStore}} implementations that use +various SQL databases to back them. + +Example of how to initialize a store database:: + + python -c 'from openid.store import sqlstore; import pysqlite2.dbapi2; sqlstore.SQLiteStore(pysqlite2.dbapi2.connect("cstore.db")).createTables()' +""" +import re +import time + +from openid.association import Association +from openid.store.interface import OpenIDStore +from openid.store import nonce + +def _inTxn(func): + def wrapped(self, *args, **kwargs): + return self._callInTransaction(func, self, *args, **kwargs) + + if hasattr(func, '__name__'): + try: + wrapped.__name__ = func.__name__[4:] + except TypeError: + pass + + if hasattr(func, '__doc__'): + wrapped.__doc__ = func.__doc__ + + return wrapped + +class SQLStore(OpenIDStore): + """ + This is the parent class for the SQL stores, which contains the + logic common to all of the SQL stores. + + The table names used are determined by the class variables + C{L{associations_table}} and + C{L{nonces_table}}. To change the name of the tables used, pass + new table names into the constructor. + + To create the tables with the proper schema, see the + C{L{createTables}} method. + + This class shouldn't be used directly. Use one of its subclasses + instead, as those contain the code necessary to use a specific + database. + + All methods other than C{L{__init__}} and C{L{createTables}} + should be considered implementation details. + + + @cvar associations_table: This is the default name of the table to + keep associations in + + @cvar nonces_table: This is the default name of the table to keep + nonces in. + + + @sort: __init__, createTables + """ + + associations_table = 'oid_associations' + nonces_table = 'oid_nonces' + + def __init__(self, conn, associations_table=None, nonces_table=None): + """ + This creates a new SQLStore instance. It requires an + established database connection be given to it, and it allows + overriding the default table names. + + + @param conn: This must be an established connection to a + database of the correct type for the SQLStore subclass + you're using. + + @type conn: A python database API compatible connection + object. + + + @param associations_table: This is an optional parameter to + specify the name of the table used for storing + associations. The default value is specified in + C{L{SQLStore.associations_table}}. + + @type associations_table: C{str} + + + @param nonces_table: This is an optional parameter to specify + the name of the table used for storing nonces. The + default value is specified in C{L{SQLStore.nonces_table}}. + + @type nonces_table: C{str} + """ + self.conn = conn + self.cur = None + self._statement_cache = {} + self._table_names = { + 'associations': associations_table or self.associations_table, + 'nonces': nonces_table or self.nonces_table, + } + self.max_nonce_age = 6 * 60 * 60 # Six hours, in seconds + + # DB API extension: search for "Connection Attributes .Error, + # .ProgrammingError, etc." in + # http://www.python.org/dev/peps/pep-0249/ + if (hasattr(self.conn, 'IntegrityError') and + hasattr(self.conn, 'OperationalError')): + self.exceptions = self.conn + + if not (hasattr(self.exceptions, 'IntegrityError') and + hasattr(self.exceptions, 'OperationalError')): + raise RuntimeError("Error using database connection module " + "(Maybe it can't be imported?)") + + def blobDecode(self, blob): + """Convert a blob as returned by the SQL engine into a str object. + + str -> str""" + return blob + + def blobEncode(self, s): + """Convert a str object into the necessary object for storing + in the database as a blob.""" + return s + + def _getSQL(self, sql_name): + try: + return self._statement_cache[sql_name] + except KeyError: + sql = getattr(self, sql_name) + sql %= self._table_names + self._statement_cache[sql_name] = sql + return sql + + def _execSQL(self, sql_name, *args): + sql = self._getSQL(sql_name) + # Kludge because we have reports of postgresql not quoting + # arguments if they are passed in as unicode instead of str. + # Currently the strings in our tables just have ascii in them, + # so this ought to be safe. + def unicode_to_str(arg): + if isinstance(arg, unicode): + return str(arg) + else: + return arg + str_args = map(unicode_to_str, args) + self.cur.execute(sql, str_args) + + def __getattr__(self, attr): + # if the attribute starts with db_, use a default + # implementation that looks up the appropriate SQL statement + # as an attribute of this object and executes it. + if attr[:3] == 'db_': + sql_name = attr[3:] + '_sql' + def func(*args): + return self._execSQL(sql_name, *args) + setattr(self, attr, func) + return func + else: + raise AttributeError('Attribute %r not found' % (attr,)) + + def _callInTransaction(self, func, *args, **kwargs): + """Execute the given function inside of a transaction, with an + open cursor. If no exception is raised, the transaction is + comitted, otherwise it is rolled back.""" + # No nesting of transactions + self.conn.rollback() + + try: + self.cur = self.conn.cursor() + try: + ret = func(*args, **kwargs) + finally: + self.cur.close() + self.cur = None + except: + self.conn.rollback() + raise + else: + self.conn.commit() + + return ret + + def txn_createTables(self): + """ + This method creates the database tables necessary for this + store to work. It should not be called if the tables already + exist. + """ + self.db_create_nonce() + self.db_create_assoc() + + createTables = _inTxn(txn_createTables) + + def txn_storeAssociation(self, server_url, association): + """Set the association for the server URL. + + Association -> NoneType + """ + a = association + self.db_set_assoc( + server_url, + a.handle, + self.blobEncode(a.secret), + a.issued, + a.lifetime, + a.assoc_type) + + storeAssociation = _inTxn(txn_storeAssociation) + + def txn_getAssociation(self, server_url, handle=None): + """Get the most recent association that has been set for this + server URL and handle. + + str -> NoneType or Association + """ + if handle is not None: + self.db_get_assoc(server_url, handle) + else: + self.db_get_assocs(server_url) + + rows = self.cur.fetchall() + if len(rows) == 0: + return None + else: + associations = [] + for values in rows: + assoc = Association(*values) + assoc.secret = self.blobDecode(assoc.secret) + if assoc.getExpiresIn() == 0: + self.txn_removeAssociation(server_url, assoc.handle) + else: + associations.append((assoc.issued, assoc)) + + if associations: + associations.sort() + return associations[-1][1] + else: + return None + + getAssociation = _inTxn(txn_getAssociation) + + def txn_removeAssociation(self, server_url, handle): + """Remove the association for the given server URL and handle, + returning whether the association existed at all. + + (str, str) -> bool + """ + self.db_remove_assoc(server_url, handle) + return self.cur.rowcount > 0 # -1 is undefined + + removeAssociation = _inTxn(txn_removeAssociation) + + def txn_useNonce(self, server_url, timestamp, salt): + """Return whether this nonce is present, and if it is, then + remove it from the set. + + str -> bool""" + if abs(timestamp - time.time()) > nonce.SKEW: + return False + + try: + self.db_add_nonce(server_url, timestamp, salt) + except self.exceptions.IntegrityError: + # The key uniqueness check failed + return False + else: + # The nonce was successfully added + return True + + useNonce = _inTxn(txn_useNonce) + + def txn_cleanupNonces(self): + self.db_clean_nonce(int(time.time()) - nonce.SKEW) + return self.cur.rowcount + + cleanupNonces = _inTxn(txn_cleanupNonces) + + def txn_cleanupAssociations(self): + self.db_clean_assoc(int(time.time())) + return self.cur.rowcount + + cleanupAssociations = _inTxn(txn_cleanupAssociations) + + +class SQLiteStore(SQLStore): + """ + This is an SQLite-based specialization of C{L{SQLStore}}. + + To create an instance, see C{L{SQLStore.__init__}}. To create the + tables it will use, see C{L{SQLStore.createTables}}. + + All other methods are implementation details. + """ + + create_nonce_sql = """ + CREATE TABLE %(nonces)s ( + server_url VARCHAR, + timestamp INTEGER, + salt CHAR(40), + UNIQUE(server_url, timestamp, salt) + ); + """ + + create_assoc_sql = """ + CREATE TABLE %(associations)s + ( + server_url VARCHAR(2047), + handle VARCHAR(255), + secret BLOB(128), + issued INTEGER, + lifetime INTEGER, + assoc_type VARCHAR(64), + PRIMARY KEY (server_url, handle) + ); + """ + + set_assoc_sql = ('INSERT OR REPLACE INTO %(associations)s ' + '(server_url, handle, secret, issued, ' + 'lifetime, assoc_type) ' + 'VALUES (?, ?, ?, ?, ?, ?);') + get_assocs_sql = ('SELECT handle, secret, issued, lifetime, assoc_type ' + 'FROM %(associations)s WHERE server_url = ?;') + get_assoc_sql = ( + 'SELECT handle, secret, issued, lifetime, assoc_type ' + 'FROM %(associations)s WHERE server_url = ? AND handle = ?;') + + get_expired_sql = ('SELECT server_url ' + 'FROM %(associations)s WHERE issued + lifetime < ?;') + + remove_assoc_sql = ('DELETE FROM %(associations)s ' + 'WHERE server_url = ? AND handle = ?;') + + clean_assoc_sql = 'DELETE FROM %(associations)s WHERE issued + lifetime < ?;' + + add_nonce_sql = 'INSERT INTO %(nonces)s VALUES (?, ?, ?);' + + clean_nonce_sql = 'DELETE FROM %(nonces)s WHERE timestamp < ?;' + + def blobDecode(self, buf): + return str(buf) + + def blobEncode(self, s): + return buffer(s) + + def useNonce(self, *args, **kwargs): + # Older versions of the sqlite wrapper do not raise + # IntegrityError as they should, so we have to detect the + # message from the OperationalError. + try: + return super(SQLiteStore, self).useNonce(*args, **kwargs) + except self.exceptions.OperationalError, why: + if re.match('^columns .* are not unique$', why[0]): + return False + else: + raise + +class MySQLStore(SQLStore): + """ + This is a MySQL-based specialization of C{L{SQLStore}}. + + Uses InnoDB tables for transaction support. + + To create an instance, see C{L{SQLStore.__init__}}. To create the + tables it will use, see C{L{SQLStore.createTables}}. + + All other methods are implementation details. + """ + + try: + import MySQLdb as exceptions + except ImportError: + exceptions = None + + create_nonce_sql = """ + CREATE TABLE %(nonces)s ( + server_url BLOB NOT NULL, + timestamp INTEGER NOT NULL, + salt CHAR(40) NOT NULL, + PRIMARY KEY (server_url(255), timestamp, salt) + ) + ENGINE=InnoDB; + """ + + create_assoc_sql = """ + CREATE TABLE %(associations)s + ( + server_url BLOB NOT NULL, + handle VARCHAR(255) NOT NULL, + secret BLOB NOT NULL, + issued INTEGER NOT NULL, + lifetime INTEGER NOT NULL, + assoc_type VARCHAR(64) NOT NULL, + PRIMARY KEY (server_url(255), handle) + ) + ENGINE=InnoDB; + """ + + set_assoc_sql = ('REPLACE INTO %(associations)s ' + 'VALUES (%%s, %%s, %%s, %%s, %%s, %%s);') + get_assocs_sql = ('SELECT handle, secret, issued, lifetime, assoc_type' + ' FROM %(associations)s WHERE server_url = %%s;') + get_expired_sql = ('SELECT server_url ' + 'FROM %(associations)s WHERE issued + lifetime < %%s;') + + get_assoc_sql = ( + 'SELECT handle, secret, issued, lifetime, assoc_type' + ' FROM %(associations)s WHERE server_url = %%s AND handle = %%s;') + remove_assoc_sql = ('DELETE FROM %(associations)s ' + 'WHERE server_url = %%s AND handle = %%s;') + + clean_assoc_sql = 'DELETE FROM %(associations)s WHERE issued + lifetime < %%s;' + + add_nonce_sql = 'INSERT INTO %(nonces)s VALUES (%%s, %%s, %%s);' + + clean_nonce_sql = 'DELETE FROM %(nonces)s WHERE timestamp < %%s;' + + def blobDecode(self, blob): + if type(blob) is str: + # Versions of MySQLdb >= 1.2.2 + return blob + else: + # Versions of MySQLdb prior to 1.2.2 (as far as we can tell) + return blob.tostring() + +class PostgreSQLStore(SQLStore): + """ + This is a PostgreSQL-based specialization of C{L{SQLStore}}. + + To create an instance, see C{L{SQLStore.__init__}}. To create the + tables it will use, see C{L{SQLStore.createTables}}. + + All other methods are implementation details. + """ + + try: + import psycopg as exceptions + except ImportError: + # psycopg2 has the dbapi extension where the exception classes + # are available on the connection object. A psycopg2 + # connection will use the correct exception classes because of + # this, and a psycopg connection will fall through to use the + # psycopg imported above. + exceptions = None + + create_nonce_sql = """ + CREATE TABLE %(nonces)s ( + server_url VARCHAR(2047) NOT NULL, + timestamp INTEGER NOT NULL, + salt CHAR(40) NOT NULL, + PRIMARY KEY (server_url, timestamp, salt) + ); + """ + + create_assoc_sql = """ + CREATE TABLE %(associations)s + ( + server_url VARCHAR(2047) NOT NULL, + handle VARCHAR(255) NOT NULL, + secret BYTEA NOT NULL, + issued INTEGER NOT NULL, + lifetime INTEGER NOT NULL, + assoc_type VARCHAR(64) NOT NULL, + PRIMARY KEY (server_url, handle), + CONSTRAINT secret_length_constraint CHECK (LENGTH(secret) <= 128) + ); + """ + + def db_set_assoc(self, server_url, handle, secret, issued, lifetime, assoc_type): + """ + Set an association. This is implemented as a method because + REPLACE INTO is not supported by PostgreSQL (and is not + standard SQL). + """ + result = self.db_get_assoc(server_url, handle) + rows = self.cur.fetchall() + if len(rows): + # Update the table since this associations already exists. + return self.db_update_assoc(secret, issued, lifetime, assoc_type, + server_url, handle) + else: + # Insert a new record because this association wasn't + # found. + return self.db_new_assoc(server_url, handle, secret, issued, + lifetime, assoc_type) + + new_assoc_sql = ('INSERT INTO %(associations)s ' + 'VALUES (%%s, %%s, %%s, %%s, %%s, %%s);') + update_assoc_sql = ('UPDATE %(associations)s SET ' + 'secret = %%s, issued = %%s, ' + 'lifetime = %%s, assoc_type = %%s ' + 'WHERE server_url = %%s AND handle = %%s;') + get_assocs_sql = ('SELECT handle, secret, issued, lifetime, assoc_type' + ' FROM %(associations)s WHERE server_url = %%s;') + get_expired_sql = ('SELECT server_url ' + 'FROM %(associations)s WHERE issued + lifetime < %%s;') + + get_assoc_sql = ( + 'SELECT handle, secret, issued, lifetime, assoc_type' + ' FROM %(associations)s WHERE server_url = %%s AND handle = %%s;') + remove_assoc_sql = ('DELETE FROM %(associations)s ' + 'WHERE server_url = %%s AND handle = %%s;') + + clean_assoc_sql = 'DELETE FROM %(associations)s WHERE issued + lifetime < %%s;' + + add_nonce_sql = 'INSERT INTO %(nonces)s VALUES (%%s, %%s, %%s);' + + clean_nonce_sql = 'DELETE FROM %(nonces)s WHERE timestamp < %%s;' + + def blobEncode(self, blob): + try: + from psycopg2 import Binary + except ImportError: + from psycopg import Binary + + return Binary(blob) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/__init__.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/cryptutil.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/cryptutil.py new file mode 100644 index 0000000..753596c --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/cryptutil.py @@ -0,0 +1,108 @@ +import sys +import random +import os.path + +from openid import cryptutil + +# Most of the purpose of this test is to make sure that cryptutil can +# find a good source of randomness on this machine. + +def test_cryptrand(): + # It's possible, but HIGHLY unlikely that a correct implementation + # will fail by returning the same number twice + + s = cryptutil.getBytes(32) + t = cryptutil.getBytes(32) + assert len(s) == 32 + assert len(t) == 32 + assert s != t + + a = cryptutil.randrange(2L ** 128) + b = cryptutil.randrange(2L ** 128) + assert type(a) is long + assert type(b) is long + assert b != a + + # Make sure that we can generate random numbers that are larger + # than platform int size + cryptutil.randrange(long(sys.maxint) + 1L) + +def test_reversed(): + if hasattr(cryptutil, 'reversed'): + cases = [ + ('', ''), + ('a', 'a'), + ('ab', 'ba'), + ('abc', 'cba'), + ('abcdefg', 'gfedcba'), + ([], []), + ([1], [1]), + ([1,2], [2,1]), + ([1,2,3], [3,2,1]), + (range(1000), range(999, -1, -1)), + ] + + for case, expected in cases: + expected = list(expected) + actual = list(cryptutil.reversed(case)) + assert actual == expected, (case, expected, actual) + twice = list(cryptutil.reversed(actual)) + assert twice == list(case), (actual, case, twice) + +def test_binaryLongConvert(): + MAX = sys.maxint + for iteration in xrange(500): + n = 0L + for i in range(10): + n += long(random.randrange(MAX)) + + s = cryptutil.longToBinary(n) + assert type(s) is str + n_prime = cryptutil.binaryToLong(s) + assert n == n_prime, (n, n_prime) + + cases = [ + ('\x00', 0L), + ('\x01', 1L), + ('\x7F', 127L), + ('\x00\xFF', 255L), + ('\x00\x80', 128L), + ('\x00\x81', 129L), + ('\x00\x80\x00', 32768L), + ('OpenID is cool', 1611215304203901150134421257416556L) + ] + + for s, n in cases: + n_prime = cryptutil.binaryToLong(s) + s_prime = cryptutil.longToBinary(n) + assert n == n_prime, (s, n, n_prime) + assert s == s_prime, (n, s, s_prime) + +def test_longToBase64(): + f = file(os.path.join(os.path.dirname(__file__), 'n2b64')) + try: + for line in f: + parts = line.strip().split(' ') + assert parts[0] == cryptutil.longToBase64(long(parts[1])) + finally: + f.close() + +def test_base64ToLong(): + f = file(os.path.join(os.path.dirname(__file__), 'n2b64')) + try: + for line in f: + parts = line.strip().split(' ') + assert long(parts[1]) == cryptutil.base64ToLong(parts[0]) + finally: + f.close() + + +def test(): + test_reversed() + test_binaryLongConvert() + test_cryptrand() + test_longToBase64() + test_base64ToLong() + +if __name__ == '__main__': + test() diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/data/accept.txt b/desktop/core/ext-py/python-openid-2.2.5/openid/test/data/accept.txt new file mode 100644 index 0000000..0853321 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/data/accept.txt @@ -0,0 +1,118 @@ +# Accept: [Accept: header value from RFC2616, +# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html] +# Available: [whitespace-separated content types] +# Expected: [Accept-header like list, containing the available content +# types with their q-values] + +Accept: */* +Available: text/plain +Expected: text/plain; q=1.0 + +Accept: */* +Available: text/plain, text/html +Expected: text/plain; q=1.0, text/html; q=1.0 + +# The order matters +Accept: */* +Available: text/html, text/plain +Expected: text/html; q=1.0, text/plain; q=1.0 + +Accept: text/*, */*; q=0.9 +Available: text/plain, image/jpeg +Expected: text/plain; q=1.0, image/jpeg; q=0.9 + +Accept: text/*, */*; q=0.9 +Available: image/jpeg, text/plain +Expected: text/plain; q=1.0, image/jpeg; q=0.9 + +# wildcard subtypes still reject differing main types +Accept: text/* +Available: image/jpeg, text/plain +Expected: text/plain; q=1.0 + +Accept: text/html +Available: text/html +Expected: text/html; q=1.0 + +Accept: text/html, text/* +Available: text/html +Expected: text/html; q=1.0 + +Accept: text/html, text/* +Available: text/plain, text/html +Expected: text/plain; q=1.0, text/html; q=1.0 + +Accept: text/html, text/*; q=0.9 +Available: text/plain, text/html +Expected: text/html; q=1.0, text/plain; q=0.9 + +# If a more specific type has a higher q-value, then the higher value wins +Accept: text/*; q=0.9, text/html +Available: text/plain, text/html +Expected: text/html; q=1.0, text/plain; q=0.9 + +Accept: */*, text/*; q=0.9, text/html; q=0.1 +Available: text/plain, text/html, image/monkeys +Expected: image/monkeys; q=1.0, text/plain; q=0.9, text/html; q=0.1 + +Accept: text/*, text/html; q=0 +Available: text/html +Expected: + +Accept: text/*, text/html; q=0 +Available: text/html, text/plain +Expected: text/plain; q=1.0 + +Accept: text/html +Available: text/plain +Expected: + +Accept: application/xrds+xml, text/html; q=0.9 +Available: application/xrds+xml, text/html +Expected: application/xrds+xml; q=1.0, text/html; q=0.9 + +Accept: application/xrds+xml, */*; q=0.9 +Available: application/xrds+xml, text/html +Expected: application/xrds+xml; q=1.0, text/html; q=0.9 + +Accept: application/xrds+xml, application/xhtml+xml; q=0.9, text/html; q=0.8, text/xml; q=0.7 +Available: application/xrds+xml, text/html +Expected: application/xrds+xml; q=1.0, text/html; q=0.8 + +# See http://www.rfc-editor.org/rfc/rfc3023.txt, section A.13 +Accept: application/xrds +Available: application/xrds+xml +Expected: + +Accept: application/xrds+xml +Available: application/xrds +Expected: + +Accept: application/xml +Available: application/xrds+xml +Expected: + +Available: application/xrds+xml +Accept: application/xml +Expected: + + + +################################################# +# The tests below this line are documentation of how this library +# works. If the implementation changes, it's acceptable to change the +# test to reflect that. These are specified so that we can make sure +# that the current implementation actually works the way that we +# expect it to given these inputs. + +Accept: text/html;level=1 +Available: text/html +Expected: text/html; q=1.0 + +Accept: text/html; level=1, text/html; level=9; q=0.1 +Available: text/html +Expected: text/html; q=1.0 + +Accept: text/html; level=9; q=0.1, text/html; level=1 +Available: text/html +Expected: text/html; q=1.0 diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/data/example-xrds.xml b/desktop/core/ext-py/python-openid-2.2.5/openid/test/data/example-xrds.xml new file mode 100644 index 0000000..101ba3b --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/data/example-xrds.xml @@ -0,0 +1,14 @@ + + + + + + + http://example.com/ + http://www.openidenabled.com/ + + + + diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/data/openid-1.2-consumer-sqlitestore.db b/desktop/core/ext-py/python-openid-2.2.5/openid/test/data/openid-1.2-consumer-sqlitestore.db new file mode 100644 index 0000000000000000000000000000000000000000..11444b26b941d8fc537fc4ed8c4a27f608f1c8a8 GIT binary patch literal 7168 zcmeI0&rcIU6vuZuOP7Ge1P?W)ge8WgRerU!AciTb` zV-5d-F~*a|7%$!on0PnA#Dw5U{{jsc#dz_6iTi`L6PsY7CJ@+{?e3d5oqg~7ncbPD zQM#n=gZ=2f)yF2W5$4Dcf z1SyV}l_gP|qbSO#veAx`*`pq^#oW6D8Ao$7SZ9iAkfO-EfK)-23`Jy{E>lJJmMKeT zaXOkwi$!tXHeZLkBqf2QxQI)=em~KvU_OmiQ>YH5G)z#G434wmYt!5i7b(R>VIHeO z5|{TFRYuj7R4G}&N8FJB&AJp^sJn?ZD?FgwOWPe&&4*DYX_%zRNJZWb7)L_`$X_qg zmSl;?24~Z=l@T`Mzp*uftntA7;c>ysDItxO0u%FjX}8%OLk)*1F|(Q)OkaPW=^E_> z;8#jJEVBq$1Wq=AvjEo7nb`$(LC#HZpSX=GO)r4oDebV#B480X`2^0uI#5^wfWH4j z+j9!uhPB$oGK;`5B(MmXu@N5)>l3P)>TYe7Q&Exd??q<)o?t>%yX)#0&VfAiFE z(6hGc^W%WeAM>xRhI}Y{E7#MrcHzyp?bmp(&v|w8$NN{0E2Zw;&)P1y2|nH}4oujX zFqtct%e5p|3K8V5^ix=fgT0)|s%S?PAuO*fgJ)RD4ho`fvE70wa$>UjtM^mwOj^_tBJ|PWt zcXjzZJ~ZDJKzFvzJ$b~uNHk4;*`EgQ1}_eV_O%)w)Gj%THR$_4giZ=N;WLPi;nZ6t zSp<%VzyLiq0{(Hufgw6vyvg4!CAOP1Muj^5^Acg~|Jt8^mRSTW0;iCGzW-bA|0$e! KOR_~kBk&7jNWw4x literal 0 HcmV?d00001 diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test1-discover.txt b/desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test1-discover.txt new file mode 100644 index 0000000..7ec9b87 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test1-discover.txt @@ -0,0 +1,137 @@ +equiv +Status: 200 OK +Content-Type: text/html + + + + +Joe Schmoe's Homepage + + +

    Joe Schmoe's Homepage

    +

    Blah blah blah blah blah blah blah

    + + + +header +Status: 200 OK +Content-Type: text/html +YADIS_HEADER: URL_BASE/xrds + + + +Joe Schmoe's Homepage + + +

    Joe Schmoe's Homepage

    +

    Blah blah blah blah blah blah blah

    + + +xrds +Status: 200 OK +Content-Type: application/xrds+xml + + + +xrds_ctparam +Status: 200 OK +Content-Type: application/xrds+xml; charset=UTF8 + + + +xrds_ctcase +Status: 200 OK +Content-Type: appliCATION/XRDS+xml + + + +xrds_html +Status: 200 OK +Content-Type: text/html + + + +redir_equiv +Status: 302 Found +Content-Type: text/plain +Location: URL_BASE/equiv + +You are presently being redirected. + +redir_header +Status: 302 Found +Content-Type: text/plain +Location: URL_BASE/header + +You are presently being redirected. + +redir_xrds +Status: 302 Found +Content-Type: application/xrds+xml +Location: URL_BASE/xrds + + + +redir_xrds_html +Status: 302 Found +Content-Type: text/plain +Location: URL_BASE/xrds_html + +You are presently being redirected. + +redir_redir_equiv +Status: 302 Found +Content-Type: text/plain +Location: URL_BASE/redir_equiv + +You are presently being redirected. + +lowercase_header +Status: 200 OK +Content-Type: text/html +x-xrds-location: URL_BASE/xrds + + + +Joe Schmoe's Homepage + + +

    Joe Schmoe's Homepage

    +

    Blah blah blah blah blah blah blah

    + + +404_server_response +Status: 404 Not Found + +EEk! + +500_server_response +Status: 500 Server error + +EEk! + +201_server_response +Status: 201 Created + +EEk! + +404_with_header +Status: 404 Not Found +YADIS_HEADER: URL_BASE/xrds + +EEk! + +404_with_meta +Status: 404 Not Found +Content-Type: text/html + + + + +Joe Schmoe's Homepage + + +

    Joe Schmoe's Homepage

    +

    Blah blah blah blah blah blah blah

    + + diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test1-parsehtml.txt b/desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test1-parsehtml.txt new file mode 100644 index 0000000..feee692 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/data/test1-parsehtml.txt @@ -0,0 +1,152 @@ +found + + + +found + + + +found + + + +found + + + +found + + + +found + + + +found + + + +found + + + +EOF + + + + +Name: Link inside comment inside head inside html + + + + + + +Name: Link inside of head after short head + + + + + + + +Name: Plain vanilla +Link: + + + + + + +Name: Ignore tags in the namespace +Link*: + + + + + + + + +Name: Short link tag +Link: + + + + + + +Name: Spaces in the HTML tag +Link: + + + + + + +Name: Spaces in the head tag +Link: + + + + + + +Name: Spaces in the link tag +Link: + + + + + + +Name: No whitespace +Link: + + + + +Name: Closed head tag +Link: + + + + + + + +Name: One good, one bad (after close head) +Link: + + + + + + + + +Name: One good, one bad (after open body) +Link: + + + + + + + + +Name: ill formed (missing close head) +Link: + + + + + + + +Name: Ill formed (no close head, link after ) +Link: + + + + + + + + +Name: Ignore random tags inside of html +Link: + + + + + +<link> + + +Name: case-folding +Link*: + +<HtMl> +<hEaD> +<LiNk> + + +Name: unexpected tags +Link: + +<butternut> +<html> +<summer> +<head> +<turban> +<link> + + +Name: un-closed script tags +Link*: + +<html> +<head> +<script> +<link> + + +Name: un-closed script tags (no whitespace) +Link*: + +<html><head><script><link> + + +Name: un-closed comment +Link*: + +<html> +<head> +<!-- +<link> + + +Name: un-closed CDATA +Link*: + +<html> +<head> +<![CDATA[ +<link> + + +Name: cdata-like +Link*: + +<html> +<head> +<![ACORN[ +<link> +]]> + + +Name: comment close only +Link: + +<html> +<head> +<link> +--> + + +Name: Vanilla, two links +Link: +Link: + +<html> +<head> +<link> +<link> + + +Name: extra tag, two links +Link: +Link: + +<html> +<gold nugget> +<head> +<link> +<link> + + +Name: case-fold, body ends, two links +Link: +Link*: + +<html> +<head> +<link> +<LiNk> +<body> +<link> + + +Name: simple, non-quoted rel +Link: rel=openid.server + +<html><head><link rel=openid.server> + + +Name: short tag has rel +Link: rel=openid.server + +<html><head><link rel=openid.server/> + + +Name: short tag w/space has rel +Link: rel=openid.server + +<html><head><link rel=openid.server /> + + +Name: extra non-attribute, has rel +Link: rel=openid.server + +<html><head><link hubbard rel=openid.server> + + +Name: non-attr, has rel, short +Link: rel=openid.server + +<html><head><link hubbard rel=openid.server/> + + +Name: non-attr, has rel, short, space +Link: rel=openid.server + +<html><head><link hubbard rel=openid.server /> + + +Name: misplaced slash has rel +Link: rel=openid.server + +<html><head><link / rel=openid.server> + + +Name: quoted rel +Link: rel=openid.server + +<html><head><link rel="openid.server"> + + +Name: single-quoted rel +Link: rel=openid.server + +<html><head><link rel='openid.server'> + + +Name: two links w/ rel +Link: x=y +Link: a=b + +<html><head><link x=y><link a=b> + + +Name: non-entity +Link: x=&y + +<html><head><link x=&y> + + +Name: quoted non-entity +Link: x=&y + +<html><head><link x="&y"> + + +Name: quoted entity +Link: x=& + +<html><head><link x="&"> + + +Name: entity not processed +Link: x= + +<html><head><link x=""> + + +Name: < +Link: x=< + +<html><head><link x="<"> + + +Name: > +Link: x=> + +<html><head><link x=">"> + + +Name: " +Link: x=" + +<html><head><link x="""> + + +Name: &" +Link: x=&" + +<html><head><link x="&""> + + +Name: mixed entity and non-entity +Link: x=&"…> + +<html><head><link x="&"…>"> + + +Name: mixed entity and non-entity (w/normal chars) +Link: x=x&"…>x + +<html><head><link x="x&"…>x"> + + +Name: broken tags +Link*: x=y + +<html><head><link x=y<> + + +Name: missing close pointy +Link*: x=y +Link: z=y + +<html><head><link x=y<link z=y /> + + +Name: missing attribute value +Link: x=y y*= +Link: x=y + +<html><head><link x=y y=><link x=y /> + + +Name: Missing close pointy (no following) +Link*: x=y + +<html><head><link x=y + + +Name: Should be quoted +Link*: x=< + +<html><head><link x="<"> + + +Name: Should be quoted (2) +Link*: x=> + +<html><head><link x=">"> + + +Name: Repeated attribute +Link: x=y + +<html><head><link x=z x=y> + + +Name: Repeated attribute (2) +Link: x=y + +<html><head><link x=y x=y> + + +Name: Two attributes +Link: x=y y=z + +<html><head><link x=y y=z> + + +Name: Well-formed link rel="openid.server" +Link: rel=openid.server href=http://www.myopenid.com/server + +<html> + <head> + <link rel="openid.server" + href="http://www.myopenid.com/server" /> + </head> +</html> + + +Name: Well-formed link rel="openid.server" and "openid.delegate" +Link: rel=openid.server href=http://www.myopenid.com/server +Link: rel=openid.delegate href=http://example.myopenid.com/ + +<html><head><link rel="openid.server" + href="http://www.myopenid.com/server" /> + <link rel="openid.delegate" href="http://example.myopenid.com/" /> +</head></html> + + +Name: from brian's livejournal page +Link: rel=stylesheet href=http://www.livejournal.com/~serotta/res/319998/stylesheet?1130478711 type=text/css +Link: rel=openid.server href=http://www.livejournal.com/openid/server.bml + +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <link rel="stylesheet" + href="http://www.livejournal.com/~serotta/res/319998/stylesheet?1130478711" + type="text/css" /> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <meta name="foaf:maker" + content="foaf:mbox_sha1sum '12f8abdacb5b1a806711e23249da592c0d316260'" /> + <meta name="robots" content="noindex, nofollow, noarchive" /> + <meta name="googlebot" content="nosnippet" /> + <link rel="openid.server" + href="http://www.livejournal.com/openid/server.bml" /> + <title>Brian + + + +Name: non-ascii (Latin-1 or UTF8) +Link: x=® + + + + diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/n2b64 b/desktop/core/ext-py/python-openid-2.2.5/openid/test/n2b64 new file mode 100644 index 0000000..b12a246 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/n2b64 @@ -0,0 +1,650 @@ +AA== 0 +AQ== 1 +Ag== 2 +Aw== 3 +BA== 4 +BQ== 5 +Bg== 6 +Bw== 7 +CA== 8 +CQ== 9 +Cg== 10 +Cw== 11 +DA== 12 +DQ== 13 +Dg== 14 +Dw== 15 +EA== 16 +EQ== 17 +Eg== 18 +Ew== 19 +FA== 20 +FQ== 21 +Fg== 22 +Fw== 23 +GA== 24 +GQ== 25 +Gg== 26 +Gw== 27 +HA== 28 +HQ== 29 +Hg== 30 +Hw== 31 +IA== 32 +IQ== 33 +Ig== 34 +Iw== 35 +JA== 36 +JQ== 37 +Jg== 38 +Jw== 39 +KA== 40 +KQ== 41 +Kg== 42 +Kw== 43 +LA== 44 +LQ== 45 +Lg== 46 +Lw== 47 +MA== 48 +MQ== 49 +Mg== 50 +Mw== 51 +NA== 52 +NQ== 53 +Ng== 54 +Nw== 55 +OA== 56 +OQ== 57 +Og== 58 +Ow== 59 +PA== 60 +PQ== 61 +Pg== 62 +Pw== 63 +QA== 64 +QQ== 65 +Qg== 66 +Qw== 67 +RA== 68 +RQ== 69 +Rg== 70 +Rw== 71 +SA== 72 +SQ== 73 +Sg== 74 +Sw== 75 +TA== 76 +TQ== 77 +Tg== 78 +Tw== 79 +UA== 80 +UQ== 81 +Ug== 82 +Uw== 83 +VA== 84 +VQ== 85 +Vg== 86 +Vw== 87 +WA== 88 +WQ== 89 +Wg== 90 +Ww== 91 +XA== 92 +XQ== 93 +Xg== 94 +Xw== 95 +YA== 96 +YQ== 97 +Yg== 98 +Yw== 99 +ZA== 100 +ZQ== 101 +Zg== 102 +Zw== 103 +aA== 104 +aQ== 105 +ag== 106 +aw== 107 +bA== 108 +bQ== 109 +bg== 110 +bw== 111 +cA== 112 +cQ== 113 +cg== 114 +cw== 115 +dA== 116 +dQ== 117 +dg== 118 +dw== 119 +eA== 120 +eQ== 121 +eg== 122 +ew== 123 +fA== 124 +fQ== 125 +fg== 126 +fw== 127 +AIA= 128 +AIE= 129 +AII= 130 +AIM= 131 +AIQ= 132 +AIU= 133 +AIY= 134 +AIc= 135 +AIg= 136 +AIk= 137 +AIo= 138 +AIs= 139 +AIw= 140 +AI0= 141 +AI4= 142 +AI8= 143 +AJA= 144 +AJE= 145 +AJI= 146 +AJM= 147 +AJQ= 148 +AJU= 149 +AJY= 150 +AJc= 151 +AJg= 152 +AJk= 153 +AJo= 154 +AJs= 155 +AJw= 156 +AJ0= 157 +AJ4= 158 +AJ8= 159 +AKA= 160 +AKE= 161 +AKI= 162 +AKM= 163 +AKQ= 164 +AKU= 165 +AKY= 166 +AKc= 167 +AKg= 168 +AKk= 169 +AKo= 170 +AKs= 171 +AKw= 172 +AK0= 173 +AK4= 174 +AK8= 175 +ALA= 176 +ALE= 177 +ALI= 178 +ALM= 179 +ALQ= 180 +ALU= 181 +ALY= 182 +ALc= 183 +ALg= 184 +ALk= 185 +ALo= 186 +ALs= 187 +ALw= 188 +AL0= 189 +AL4= 190 +AL8= 191 +AMA= 192 +AME= 193 +AMI= 194 +AMM= 195 +AMQ= 196 +AMU= 197 +AMY= 198 +AMc= 199 +AMg= 200 +AMk= 201 +AMo= 202 +AMs= 203 +AMw= 204 +AM0= 205 +AM4= 206 +AM8= 207 +ANA= 208 +ANE= 209 +ANI= 210 +ANM= 211 +ANQ= 212 +ANU= 213 +ANY= 214 +ANc= 215 +ANg= 216 +ANk= 217 +ANo= 218 +ANs= 219 +ANw= 220 +AN0= 221 +AN4= 222 +AN8= 223 +AOA= 224 +AOE= 225 +AOI= 226 +AOM= 227 +AOQ= 228 +AOU= 229 +AOY= 230 +AOc= 231 +AOg= 232 +AOk= 233 +AOo= 234 +AOs= 235 +AOw= 236 +AO0= 237 +AO4= 238 +AO8= 239 +APA= 240 +APE= 241 +API= 242 +APM= 243 +APQ= 244 +APU= 245 +APY= 246 +APc= 247 +APg= 248 +APk= 249 +APo= 250 +APs= 251 +APw= 252 +AP0= 253 +AP4= 254 +AP8= 255 +AQA= 256 +AQE= 257 +AQI= 258 +AQM= 259 +AQQ= 260 +AQU= 261 +AQY= 262 +AQc= 263 +AQg= 264 +AQk= 265 +AQo= 266 +AQs= 267 +AQw= 268 +AQ0= 269 +AQ4= 270 +AQ8= 271 +ARA= 272 +ARE= 273 +ARI= 274 +ARM= 275 +ARQ= 276 +ARU= 277 +ARY= 278 +ARc= 279 +ARg= 280 +ARk= 281 +ARo= 282 +ARs= 283 +ARw= 284 +AR0= 285 +AR4= 286 +AR8= 287 +ASA= 288 +ASE= 289 +ASI= 290 +ASM= 291 +ASQ= 292 +ASU= 293 +ASY= 294 +ASc= 295 +ASg= 296 +ASk= 297 +ASo= 298 +ASs= 299 +ASw= 300 +AS0= 301 +AS4= 302 +AS8= 303 +ATA= 304 +ATE= 305 +ATI= 306 +ATM= 307 +ATQ= 308 +ATU= 309 +ATY= 310 +ATc= 311 +ATg= 312 +ATk= 313 +ATo= 314 +ATs= 315 +ATw= 316 +AT0= 317 +AT4= 318 +AT8= 319 +AUA= 320 +AUE= 321 +AUI= 322 +AUM= 323 +AUQ= 324 +AUU= 325 +AUY= 326 +AUc= 327 +AUg= 328 +AUk= 329 +AUo= 330 +AUs= 331 +AUw= 332 +AU0= 333 +AU4= 334 +AU8= 335 +AVA= 336 +AVE= 337 +AVI= 338 +AVM= 339 +AVQ= 340 +AVU= 341 +AVY= 342 +AVc= 343 +AVg= 344 +AVk= 345 +AVo= 346 +AVs= 347 +AVw= 348 +AV0= 349 +AV4= 350 +AV8= 351 +AWA= 352 +AWE= 353 +AWI= 354 +AWM= 355 +AWQ= 356 +AWU= 357 +AWY= 358 +AWc= 359 +AWg= 360 +AWk= 361 +AWo= 362 +AWs= 363 +AWw= 364 +AW0= 365 +AW4= 366 +AW8= 367 +AXA= 368 +AXE= 369 +AXI= 370 +AXM= 371 +AXQ= 372 +AXU= 373 +AXY= 374 +AXc= 375 +AXg= 376 +AXk= 377 +AXo= 378 +AXs= 379 +AXw= 380 +AX0= 381 +AX4= 382 +AX8= 383 +AYA= 384 +AYE= 385 +AYI= 386 +AYM= 387 +AYQ= 388 +AYU= 389 +AYY= 390 +AYc= 391 +AYg= 392 +AYk= 393 +AYo= 394 +AYs= 395 +AYw= 396 +AY0= 397 +AY4= 398 +AY8= 399 +AZA= 400 +AZE= 401 +AZI= 402 +AZM= 403 +AZQ= 404 +AZU= 405 +AZY= 406 +AZc= 407 +AZg= 408 +AZk= 409 +AZo= 410 +AZs= 411 +AZw= 412 +AZ0= 413 +AZ4= 414 +AZ8= 415 +AaA= 416 +AaE= 417 +AaI= 418 +AaM= 419 +AaQ= 420 +AaU= 421 +AaY= 422 +Aac= 423 +Aag= 424 +Aak= 425 +Aao= 426 +Aas= 427 +Aaw= 428 +Aa0= 429 +Aa4= 430 +Aa8= 431 +AbA= 432 +AbE= 433 +AbI= 434 +AbM= 435 +AbQ= 436 +AbU= 437 +AbY= 438 +Abc= 439 +Abg= 440 +Abk= 441 +Abo= 442 +Abs= 443 +Abw= 444 +Ab0= 445 +Ab4= 446 +Ab8= 447 +AcA= 448 +AcE= 449 +AcI= 450 +AcM= 451 +AcQ= 452 +AcU= 453 +AcY= 454 +Acc= 455 +Acg= 456 +Ack= 457 +Aco= 458 +Acs= 459 +Acw= 460 +Ac0= 461 +Ac4= 462 +Ac8= 463 +AdA= 464 +AdE= 465 +AdI= 466 +AdM= 467 +AdQ= 468 +AdU= 469 +AdY= 470 +Adc= 471 +Adg= 472 +Adk= 473 +Ado= 474 +Ads= 475 +Adw= 476 +Ad0= 477 +Ad4= 478 +Ad8= 479 +AeA= 480 +AeE= 481 +AeI= 482 +AeM= 483 +AeQ= 484 +AeU= 485 +AeY= 486 +Aec= 487 +Aeg= 488 +Aek= 489 +Aeo= 490 +Aes= 491 +Aew= 492 +Ae0= 493 +Ae4= 494 +Ae8= 495 +AfA= 496 +AfE= 497 +AfI= 498 +AfM= 499 +AfQ= 500 +AfU= 501 +AfY= 502 +Afc= 503 +Afg= 504 +Afk= 505 +Afo= 506 +Afs= 507 +Afw= 508 +Af0= 509 +Af4= 510 +Af8= 511 +AgA= 512 +ALDs7paJl5xPh6ORH61iDA6pONpV0rTjGiTkLEW2JsVsRKaRiS4AGn2PTR1UZXP0vXAmRXwdSegQgWPUp3Hm3RofRcDh1SykZBLif7ulau1hVO+rhwRyKc7F8F+7LcMf/v+s73eOXUDbbI2r52wfr7skZy/IELhsC8EK6HzhACI3 124241322153253947064453752054205174382289463089695815605736438952932114700118408072544073767229325045596832952652232288773280299665950768731398747700657715829631597019676014848183966683866396215048196276450953653433516126074463193382764063985175903718735372053536664711482497859539116009770850968340298474039 +AOzgU1s6Pd2IkrJlvGND8legXTe50nyDCocI5mwT9rW0YsisY5jaaEOcu51BAq9MmXBPeVX0k/jlXwH4Pn3mCpUAU1rEOsTdcmSJp35siKliDdhTZHHdZNMW+igfXGX5OCsA/BaBcGnE6NnrGWXKyTOoVUGQLEkL2T5yhNUaCT83 166340174936369324883416612727439279977041963320514134445183426741643586944819834936989524033374309932122967866930503619179389342537723598234062828695747850043368572301869699886931403612266216965783079972698791813140295203826980649434652168563255385527187360027803388963151668338040517316899628026707657178935 +AO8hrpw+lDiJ13JahLtCb1RenupQcNd0wlTSck9OLL8wB/x6gAoj0PTLV05eZIbz43N3GUSDmmckjlxdHXiBJ9rsoB0P95l1CWIV+4rXblCqxmOdmlm6VZ13bqbI0x7l0cjeMrkmk+yJ067WqUolqQBlUWMTuJVfkxALJYH5xr/C 167923899524385316022824282304301434707626789716026029252173742527362300338760906999615029022863637963070711762128687835779073122264515776657475985362344360699359591353388569856862973447791264902182048648600267737826849280828116753682917256540180401899752566540869918949003470368970029744573140084219550547906 +QxAn7yrdVs5tlHV+Glbqdaj67c6Ni8am3bBLOL8PV5HbdrLf2xIPmNugo6MfUwFSnT+ZPJ51+VTOsItaNwCFju0Eh1cqyP3JWyLRPE7emKuo6xRhf+5ik0pTg77LEF4JXW6ofDqirpR4alFi0G2d9yImQPphsYJwYGF/nNT8u0Q= 47093316905427544098193936500644355852669366083115552072584429220248776817916430034648347490325490701471113667554329499736495877969341478442613611948220957798780043076906836236556612316544460763366275536846463456405604189392790111985912854476264292503164100482712281088955640964034295834935468665872932715332 +AI9PVzrbJUvmCihwSFans1lBKwudGEZpWWu8pkSK2zVgzGhMvUoGgMp6TG2zsUd1tV8zv7KsVD2t6pXmjT1wPUynufq97GVHI06SGpflDTt30WboYRh3DgYxvso1sOjUXpnDezcaqc2Aiz4nV5MSShkBlyBjA8z2worHDE+uXqw0 100635651531872121827765663065728398779771663753008344681972226973080394359405041113312675686974926993279775427390065833081040771269307007695807025882757371805607979134114890454059957194316765342461291139168706134406917264848659448693866813989352429841300235734400772946895458374870482441457514575059390213172 +FiinVicXOqqRLpxcGTorQpSAGeQ/PfDOuzYK9ViFtmPv6D0cYPfhUH4qXEHOejvmX+0b4lfaX8MWPVZxlqpfXiU9BhG76HJxkLF4ysipukeOvhoHzvcxE5bnhSF1i//bOSifATBLBEZInwqSVg5tHHPuuCkwTL91NqhOulp7Lsk= 15560440884463435471963622630292643727112462888414585143143739400703889930416938984547754943252935620248108237258540176511252143752416771350868493435174026287082706690332705481726295797196444796135827460509780634261726494455068460028424141500629527968240913757449787164107068039175831847071025316475940056777 +aYrxyQN/hkBne2ayqo2/iDLF3DZGgk080SOMJfsj9h3Z1OfFZM7TJA+y+/O7niqatosvKrfHrAw+Qs7c6tCZ6NPwYJ4QJLOF9bqH2u2a3fkI954voNUctlUagYUJsZXV8hdhLM6NwUyIZ3ZFkPcpTZp7nKQQ84tr1a8VjDIT5/o= 74114640794666001532816944350975062126079079113921109750255283189037502412929005615388097912507598112836936032143435813588205939470002911374442844578739574773399427907766548612582213272643279263782396527705126350063372192910060171635870872236876399794128383338399728947176692692942605589343038282957050865658 +AMpCUeKUX/vtRslWiUUuXNl1KA9uDAWjMUkTrdsxxRDESI7iZIn3TR9lW+0kV5fzkLF18iYLAwSGBmX1PS/T0UVFmoBPJ9yS7yktNL0lpQ3noyGFn8HHZ6XB3FkH3jegIfGbvwwhnhhFzpHPrXlpO5iU5Y+rexzp2XHWt4yJGuIL 142031143422642739313498629438991149460874309300342349421794421544918823888598660275343727563280565210534243383322796489809683834300630555650331646026843796764549231159336347965502383849513994449309613369541991287590422095953275586374371960367000083487965487661436037637475372929033613295072397262739084075531 +AIMIQVz0JIEKEI+PREu94m3v9XoiU/Q0CpsSuqkwSSje+Wyul5ea9oU5qgtOpdkMUOW91BJo0DW/GMZ8v3C4qyyP29TtjCcAHObJi9hfLSlnTSuzXZnDStooYYKqzfToLToCaAJKCXiXAVW0vWtapLnyqafrf/KgyGZ5u4HfXKY0 92013973253053602863003242446596060337454881568126916916519869242232429836082762281129448384605359749247852792606718908482332975424967542242332487707042773885428473061056052851768940900752317020681189773407893371297668591494665352294885305475871917069040377145530889271334616499701769138948975263435137525300 +ANfP+zPBTR27afneyac1KJhOB5Pq3AXB+SoAXJvQI/GkSoNhw5KdfqoIkLcoJi8wClCm424Gm1AdrdGwDFOM/iKTSPkrvMag93+b2EbQGX66/n2X3YRFNkgq/Gtb+2M8oCcAL054Z/iiMD67aU5RWzjqS64ePHsn01bJ7dqLgpMO 151548639867177154896951257541227014781655576169318283047778755573323724856619156348444192550664853912434681577093459933599575436686424046466113215132845213008587152894642577278656978304699131916299275797578171518984206145555369576872231567191579337901913492071976578289189524123204040497290426960375042970382 +AK0kHtacLGu1NFWMADq2rG8hpzM4UEYyPOL+aMJbnwXcUYptRIxb0YFZg35RN/RiZs4lQsiq+kEJKzMMV71TsJq59vMkIZhZoB3t8g9ZqBZuq0JYcTICDwRpNSttJidVpJ6P9sR3s1xPMYKdlSwt6EEc9htOXfZU+yHKYgn98X60 121583812047864398969816595368193171848971298823388059338224714026742264861090347096116404814514279627148994345584790617974476594451626305761040465570524035369799925437276511604752129817947910677564301623631349399504187314174538914591944778074509068973226322566160587813128746039859381466427380402262866230964 +W3sZlWW1Aev3x/DiH9MzwCAZzBj++x9cknLfGAHwgFqkLH6vimEH/r8hi85hzlCOG5CjwhoZ0D/Hsfr26ZJ3X4chG84byrfDnek1V9mm1++v+clJvlYgcuVgn2Opsba2TILTm1MDB+Ujs9brJ2AAKrE9+ep5nvtQVeG9PUGtdlo= 64240043913835461386212515483198059541440539167395859777194837833769712010594411295323900074066077107346806786205590345517755715510695858065925747020336398305793661773798243627926904542715123849691490667964262778804487343218972081260210371192903128886030021862362141928329650003493687310970684093289133340250 +FTQRk9/BIj21gbLwI22fHJWYj+8Ghdcc613hOtJ+/hQmh73HwTXLpaGK9aCptxVbpjW0r/bxaRjmgxu9u1CCZh5yRd7Z46Wk/LIPXGd3ycQzqRMFB7TISFQGJIcFoxRp3Eb5wa2OyrUg7c/D+kb7oFJq9P7mEwIh8TpLzwmu4SU= 14889529068556301710329043521845510156960298822469914567758538023025100741826628180855835334285179977296740667353391766487166458692144569279381035582718738461626140662441222061900764829681913534146898551570916312642104487829660946024590782808750587095559047648957238487820069966851521487428624726655438348581 +APYXO6uGvs9qWiEAkcWsaCaCrGJJCP2Z1g++XlJ67oZIgEoWITn3T/R2/c4edAfwUUzNHAYZN1h2dSrRoqlrRXrbxFtGOuRCUrXcGLFFcEbTrtm+z5z8xGRfcorx7Cu3FIBPMK5XcGPcbRZdyP1gBkeDMvuBNAo0/To+LP/dhCNM 172810804474418448604443090732221483571611665465870520701624598983692130272337358406272727413570938793741430131635927237320173996217984860203754686741782921346604605228620148450611724714868551781003004492587584071978757421616871762681049508123223983431502852926521520561941051298696758046005573332373854233420 +AIDNxhnDEe1kTJ3XGfTS8zKXeXPRdw5yifm8j8Ibzj/quExy7hFPtKct8hRskPR2qwTlMiW9Ra8Npg2USsqHV0rBoIkX7E3psxq5LBfp/q00l3SEBuLL4K2FGR87bPgU+Duk3RVrNMnColiTcnAR5XkoeWhn/r9MfJMIN9Y0FEh8 90449107125498302548188660544012777357148118984122992664008792590422284061463729084479315745509706793674355738023180454297730948397413371686013210006834869294564190666543874617716180411178090109573192518129248278410216362657350215009192850017507998797754539132540293137589672869131300859207213449571846080636 +AIRWavxYRsGlH0Yr0DudwrpYtbrByf9ZsDawKom7ubiRfepqYzcBlwt4adMMnkYSaXeYtOsD4KBm2ZvLKN3++RkYNmxgkyarORBEg7ERyiThBj7Ksw57pNHCAoHtBEhH7Wp9mHhuZtPvPgCEptmwCu9rYhLt4zZp+Zq8a02dkXvM 92930601962515884925250459851491509622611227724602941760145671636277317511265759558869239180653492283311584982044597979173761619470326096725838197524704577188104121460089235709339932110663536557497112887112782062772810759971739760085128369628777812332518137107605855679096146402427144185104230596200130247628 +AMNJGLcAiJtL5fUfkesWKYJurdYSnvsOZeZcrg7bemkEVjF6S9CcojimUl+ncr/YY5/EXnU0mg84fObtDxWWdJ7z7l0CFcoALTyEatDYKshT0xvdKY3u+LUShxIAyk8EcGnf+KoEaa4Mx3tg2oTBnVegXClOakNTWw8bu2ItagoQ 137134165107366719462230252606689766470445826753581409513106273517221906418464863733870948759313279128624638614534848890858250894834883265387344539280755177217350585564186248554307335197387734431939154077778003706720017441895613190141376534460438929588407764609772857975000507660651583780079804513519571438096 +BmGPZt8XqqI1PuLN4K1/PZMi2rfOYtHEMrcwZdSjKRm5qTkd0Pbb/5zPV07TnM0uLRvIQYTLloEY+GYyn0K5gDTEZpEtQ8ee6Y87zYGDwcf20eqYNxkA7FVV71vqCP/Uw3Oi6B+hMvsWZbvv2vH6MkAeADCrezOtwqVS+irftyc= 4480956865245875120472829476982311611308898564405318773810939350829150182630548948231116574193987272498161864310429976564278532538229396846813874244969927890037756969704618336242255039858182439641759659872128285423988638335967412040624105824571426792562334458751137508116412821914961236269913776304372561703 +APqFgCIYbJWuRyEGuOStPvcprj2PILQ0JpgwQ2jLKn3DvkWSd83qh7PWGKozGavsjh803K+ZzI7P2wP+Nc0r0El3q4nzaHvKaCtVRyMwbXv9wYLFZICeM6J1l9ljUMts4tbDoPzkIy3ScU7pYxarBWqMkcBU8qL6NN1vEdkeu0fW 175922170410080716883576123079908758276229469783745771772401183721225804343343063277676406040455068452258961299511343441961963941297631097736305638850193978800615558067791016294285848963023036905095022181004058235239390870177623185946205281141386416867569004073524130001309977475780893497185890756991672600534 +APA/rCcGeH6A+6ZwaBBDM6mB6tTD8mjkrOWEo/pK3MCZ+rrErMBnFp2S19GhlLOfuY8BHS+D834Fdm8+3wKYkWnXZpGb+e3v8ofOQ34G1HvzULOYtrEiC4ISZRt2SSyz2hU+PBXjVnplsHWTRxZDmBxTJdgli4ItAqxGCxj/aJ9m 168708388929747822981923386197903561880341990893945097067702518857172133291360611402092714329372304718329568897960770488377524912057166920574319430820488930520807742026377043178502591886293565177404635365772829346030773275726024973460121300339258054215286249329967181244588558220467488638468686270735376228198 +AKmwrLP108dCGWOWxE/6woJVLRi/Kra/DvdsPkkrZQmWIlUT7IvwM4gU6bUr4f6wpT08cIQls2cGh7dbSEaO0xLa3mmtKhPiAlzSnz0wuifF3JT9U3uXgUfCZuFtE0z7Oi7WTOrpl3k3GA7JFvXnY0lwblIQALVf6oWyNETnajGl 119160465301384937485959146028591622947513292915838943629387700439301197965652871741710280647524383590817798553034250156068573474278225305190573334054718387045488098320076877626430189054572361967283632592181431701411266656256255758079114072932140551282607247364388070762970060420036793573956057551235306893733 +VTe3rCzAL1Sljo3QAXEkAdBy1ZARHZwtrj6ZNRa5ttqd6/l21g4z3iHCeGo4rnE2F8wYTy+NlugjXw86OS+XojW5y6UzTtx0HX5IJ4POqN64aXWmaklGzroBEYWeuFFKcgQN3NOxkuJoDQ6VElP7Epz69kj5CsKJUwL0SjbNrFY= 59841866347633473702601462509811342285929528424012250265905695635971518533504187799047710303717472950129869674786231155102509311322791323986824635569605105662070745033595366004805920086888891275288347907772640070278731650628917037915863439204501060041944275512863990729926528905725569467329169134226609384534 +AIZt1xGhC/HrvpPASsvVIVdsu//tn0noyJmVYh3FdQ6yIh1uce47iCsrV1yvYqx5ZTbC0vnfnbjFcWqH+HtLX/DelgvhEwzqJ8hwQrfE1ShLG4ZjAVo1Z4GCjrDcEUMlwKcunuSJssuxeQuXwTLS92+q6QeBSS7OmfxPX29CLb4B 94399298271083745508290936113986978382457275531684761701599029877008571741877683365769553170771833981099580359640421358853566501815723434822307977440496208486103754978934472597505865596938563438311337045817621762649604204720249750058676095769230214181772215323235427976398686727606000594646472236822594174465 +NIhTPpWXS82VTA0LTd6TfM+HgLgUcmvnMYtLqPpuqCKZwalAycwl0XFYNyVvaY21J94j92ts/lRYgVtHDhk7/9nLXq5js/lsUnG8rWPHJo11JTxvW+df88aX0pw8u+biOWt87vc1MW1dsMTTsJFJAeBx77qU/Cwto95IVqM7vSE= 36889590210230649939994518345793530042252563793069578097360569338647730438860274349862767107939590441616825589851005429465345268710487649366046960918184701290985280638488938340668212498212581853679035928093386035688597446809895381618260692378376844452061580510108168030682664507293277674052032318576713776417 +KXdi4A2Z7tSiiX9YGtDtxUXIfQvPhcc48rUH+Q2SnXL7fLNmr+F4Rf3RiFBRiHKocPfE94pothop5qQJ5X221/DbEKWK6s+ChfQ636jvRxojoLMab3dPtaAPpDJHrfZMxbT4ZaDJT0tpA2e+zZrzBuDs+UUgCpty9nxtdm1gS7A= 29118662951481660380477444121362422614202367719725087486810943918529894738076273660245405874301505615796632229852040910511025841576465052938308369421493312085081188509808322692130449282585522349552501983296872614029139293444558468751646868108213623606366977549477663987815308260383403466635254115908032940976 +AIOTBZQR2EJJRmoWdRNFLG4fceoS3KnRTHRpPdllhHODqdg+QxTOcOvqIzBqgdD0JgO12SuNAjLQOiz0jhd02qkXw9Y1adGuKvL97ARFtNEuJiNzFAj7KpDLy2zk2rPJp4Lp7cjQs0fe8BQYnTzTsNRGm+4ybln/gse1YWu9w8y5 92394618277596007469808288231093678404089765494062813665106014405059399079199990128824492247005602685377185496959522609467906358619318009231448503013528692450191782140091818984176967246749464502089280153086163239846744554575017530385347720563798041108608545014076448155956762636929707905789978331102411214009 +NzfbJRBF4pqEeborJrjoknJgpfK+DZh2k9cE5dcElMPZ2Zn9im7desWGiBSQnu3KbTO4L/t4+m6nFTNcbIJnqbVSMDHdsfG72rG/t89aOuECQw0HMVVgONNNa6i/mw0jZSWnRLD4fa1YgbUlMd8jeqO9XcBDB4mVtDTxyeGa3vU= 38775530011374537813502898274019389132620116890266344603221997943675706375698597061571989090674289834838060050848545748579361837989319487970580969082824601965845786771062335733318139530316825802589479118956745739691326447349403950997231306042638797277408335778415717988679050762936401945487285814799382535925 +Y4BVPZ6necuLSwaqYEPeZp0lt9tTGFl/WCJJbwg7XpyvuwYKtzagC1NLzY5ymBfwGFw1yRlQuyGsYd9mBfC99DuVCIeh0JNrhJN1bNfoSzy5UO5+dmTr+dm66VGSRS0tFcViDTfCIleTV+zxo/xuZT+Bjxq7kZue8zGkjp42Kmo= 69872189501616471647606976308259279995249122669120675885925763529037695584466011511740991152346215507625265226811128801733353566555339153627478941716586678793853828514394269931890370517258825006937741437480128878717892485074131232336852490940507703859793477547154689914725314529986438108117871674332626168426 +AKCP9Mto4q/a2xNqM4N7PekbKspwt48OGPre+iqVwPrSP/jWKxg3CvvLNZzN5P+/FiUGIklMMFJ8w76OaHIPqKuwckj1gvCLECJEE+UAZWrNKPmpzd/ootN9/kQhNMuloTFCyhXAUUOXJ7Z0WVLb2u6fn4zroszSMBoWQEKC6lcq 112750701794692134675959811050012620191158543234019977304167102486465198271340022889272244811582365901584420008564301920174477182946432553537794834985703732129975734658113610563794129371053853971031300761815004524681756388784922001759202643614966614186697992611399618828963452661554240362943588548146868410154 +APOTAFA2waoAODECaGNgCHa8dNN+cjMnD01M+IeQFytzo9RLMzzzg/gpTUFpyLtFMcfbCkDYQMLXwE4crTimdz5sVvjGQ+5fSFQjoDY6Bw7MO6NAcLzlV/sI/1WyNBKaLQbcl2720n16tdUcdckQNnV+cC2J48CVxYM1c7QQlxA0 171043636512232272455501595416608280460445723238023572475354665686544174728784633443479486247342724860289312593374524429736857970220153680852977711594899595712511352458264354251161579203922747468321999465061463474727943140910084880926005209538535217464825087114791420210981711903880998556269523363208766099508 +AMGpxRlB8WVnsGqyyiy3/mzrPymtJW1o1HcDErK11ZwQV5PwTF3c0THwlnxDmcziLWHSWgPQwfRddVDCXMGW9BffJn+XO6aTcWDPmDAh+1DbWJPE1aqApGbHvQ8HONy90dQMZf1ayuwceWCVTuU1wnHdo9F/sIsRbuu7ic2OJDzY 135994898408425255747055209966103741651849229328236418804928584233229830656742052333413774490626915784901255640138520158698845938184666683995579777154437927013722740366497459963753542029774185193376253885864514386760437194444013834088425088260658670140534670789371556026135595577395047002643901630053097946328 +AJAw4uDYdSYkOrjtwJVWLv3pi1+UxWge4RmkWKqVquTsAVcT2tRZ+MFdHM457Hl7fmFIyxvGZQy4c2v1AbHEfPR8ID2sCRQpdcfrxEUZPMDqxfnHHm0ziny6W4X6ggdBzMp/sBWaVNTBL0e61/pELBGYNRGFMzGws7HQkr/sro1D 101254336834199527040756567675327011562230719161388328289463594628690618298993695452746353237675715087353241661592074446889034411683413957950360025295995263477031608845241728493807755308798509893719674568267846671753070163272328014412744008880395248474446310603301447848026040555910147467745595720879397834051 +AM09TdtXgYL4FI5CGNiVjf0T/AN/pZ5zZsBOi1MAUKMURiXnc1x8VKYTqM9Xb86mqNBBqphynIQG6/3e/YbGJgHlsSdrmKbo+P9daMr02I/7Z76/7Osa8+7Ky6lhVCbU3F0tBH4WvopkCQmuJ267afgvDD5kB+9uNr28deMH00cY 144124056591600568767398029380314564902309327093641173350205276895603332085753288682409279238417493662029954512382520307259348748813767324609446500382301421328754981718014234615523158887865271179104711373675849713359713282937065993613915015084108700238420759344034475478243507306107546245540340758766909867800 +AKDhK+/BKGXbrbBh2vM61OP8LN81YwlJKe68KNwUu4tjXlQg7i49Jis7QKPI/YFPUpSNTu5N2iCgeMnCX4+r3NAfivOao9lw4N3nc9bi839SIWdlokhwBHBYmCIgjehUeBAdkU4jKqlE06pIrpRmSvBtn7O4aWTbT+C++ViYAcGF 112973480670453665543892521898882856059335781900313607790238402438320486344365203510769919022496690291280873287383392088872774202832124927485754495093552572232234532821756395965072330282810574669371524103814871172318519695921477775100282448247625395376072233777533359104085023946019406729587713120941266551173 +ALxDiSxHjfxvP8ETvpE+SyDPTS7q3o3zCK519WTepygC58KSRfvDnIVIyV3toQKzgqD50kF1Ni5D/wuaSs62y3zg3kELX1g+WuBCc8+x50+kDtbHXa1Me3et/OqVS/QeppkcjK1UZMU29fXze6P/w6aQfvKQkE7koeQtZBKkYc0p 132203344567902304830160099595561253300484092355345272411265169562971473393256361094745618829297250316196312398486598077249124198329075791740755862221465178128527292695331061023291345396067863215552021206609309872689233899464919108147533679134727064586730810633196817136739658243232643507412032417747255282985 +VF0YUTvy8Mfi5o6X06DEvLm87r72mAtTdyyLNr0/GXlk0Xj3L2Oi2bVUMtcXQNRXg/mkdGP88pgdaP/eMzqkUU++vJ7t3UgOC1i3SHegpiBhhZh+aZHH/wjFV8Mz2XZB5z8MpMgN+QwALK1TT2Pyt/feQTsOy0imVanB5+OvCeQ= 59242171319056188000481457618922567543461456096441095927600135114274111606802456239311634638536207588762066940095527920532936960549439269891703098017342732142860571277442598349453761561189719823290643146391349978698217357430495238876700400634593256155537598291759795109752990651995982467695091946768443574756 +ezpwBt0N6QhTusiPcKrBvSB6yuk/KShTLUFQHdf5J1u1fgDYrp+aOWuXOFVfOd0bweiG4UxBQNXB2IDFWfYON0fBoaDqNk/41YyqXBSkKbiNWLi1y3zPmwTAiwK0PzYp2EPfk/t/j0HsDbvebu0ygcxb2tPqj4EQ1TXEdD007kU= 86533835313999945727720083706940213467453975054116752898416709637030456504024135513972566184073843025739226187558143854850980654667596935003124034699919861200483994576288766702308068265526535622439762454501169018136389983894783905946543636163866717367545972667876983557989192393479830223914708619684891389509 +U8BT26zT46tTZnkmTNxGUAlXbJhk5cNi4AMSd8fSvZHm55siMFGJ8Jl7mtdzEFR1UFAyEztf2fUhxdtMLe8ei/OJgM0j7myQ9STucEwnsShT7QS/DjBmfvcC42sl1CRpXkb0ZLrEJCPf+crtLKGrG7ExS1oawIAgALBiMQIL6mE= 58812148564290791415180898639607206220554150794356494356250223429674091688305329629529905854147200457536549527135776329004085047145097927266797668252160196098870200925284256433894773392353678965699083286106628662506590268955650280670838340651598082083455821825076016227525614626726458235627297885815646710369 +HfYii3U1SIkBZl09RHaGGA7H3np+qxyNeeCNY07PDl8LwZAaaYk/bHPeBVboan0I2X4o78zCD/gFXFBJ4rxwwUsVjHEioyO2JcpV2/oDOelJBD//78WzBMMSWt7ZKbJV9uYr9ZUM0BUD3fsk1esFCEdnDJdr86U0UMmiig2R+ME= 21039655953870571289679214995029926285040274249531458675115179004718812090027267801012507748013357317597416722235988917212676802092082137617336199787762782958420742299451435320649616271885264333948336627286638368859041172783505464468640994920853000441536629081040963398001710173320125308624362209157720438977 +AICOlee3daFyqTrTdtWjVb5M2rclh9BpIo1CRvKo2bF7NYcjrU0/VvbOnTVXDwdeGMLupbi76f0BrfDxYtkzMXvIZlgoTit4g5ennnklDHFBC5cogaGlri8U28w4/h5oMunZ1O4ezdpRgVJe9nTP/sSEMYiNS5IA7Zshdvm/XccF 90275777798511290102824338787811725003177532250296755103300529948194832904403489332420505850668003332750291879153080212231952155092379375422537931240723308384652734942204313672973885652497290433943089371705605128843469306776615573873479312715317072986990219294942040272550822460408702072075001377245051602693 +L0QUSVIjxvE201b1ztRZyOOxy8vkUz6626TH4tbLwXjjc+AhmrvplaVlavnOgHqve+/L18XNuAYP4BqdxIcWTx+yxBKm4ZS92dRJdcAtccvZpEJtYjdJvI6qbL5Ph6HluaVZwp4dyFyXuZOJGTfYdTb7PUWM0jNT/xsqyjxSQ2U= 33191267986826803728285073844005357792766429917696698533494382218509532051029343127452480789088572904364699220151221680328978554239767633887572649589456766209242252549993823283929686430100804479376247660556781589549613316880150951333982646510273364068770923588389668733632648346075516618646974067295703417701 +APlD9ECKJuACUmQUsbd2GTOpb2PgQVT08C/5hyNEVdA5bWoICX7epmoCKCybdolk+cfEBP6fSz33j+Vn8MbeiHBLdmF6ETbmcyOjldJ902MDvU8dqAa8IgEZN5Uh5x/xzN+3dqk9o0ji7yi291u90rpfIh85PPpDat2B4l5zs9i5 175040148659257809883308984693597046378367187659749953472629929701758633206586720399909808941145946314755491399962797299295431089674294356220216615950668954164397362123668926410543898553191541662075745481299747832013627018846822876386760538344447600390187421938699064459451308870669878673306013635576901916857 +KB7N0tE+A5vFhyrd/m6Qe1wTihkjqmBn+rinfmMAzRlvtxIBSyDLzQsOQs7L4oTG64ABU+YwcWVijvoeZNamaxGl4hatAH1pRqmC/r8FMvC4vqiFTbFHzQhkjM7uoHD1aKnxyBVgjMj0E0KZjrRxydZjIR2p13FXjLP3UQSFtII= 28173452509830313810392326357601136401754938805266458365469366750775669869895498658593356375710132149836430968810246171974040975430205200958564616924399794768861923079158311829444850822144940112488994119845741191519421434257276977333662656888696213514226866147767570046232093727585815615828360199830275208322 +bxFgV7eXwnbQScl4VzS3RTdcMW+NY6pcGkT1UsqHIeDVyBb8DnH/2/Z+DX3zniR1iW6FPdvhJJeQyPIax1ohILa11R27C1TLxGvTrRBGUycxjEcBIxamHveBsXbECWusYLEakeSDg9x4BTWMz1rTQajkorBoeEjYuW+xBxQtXME= 77994515143740690952370766995249847650881300682406161400195705464876513409097078624084133111941171517535435606295232558665316819077765607639545069239931096306624817379462598756505457054433358548941076472902905065316335603665413114267741896000877284610377452471067725794013283338924419969559537339967562669249 +AOH6E2eBzD76QdTJ6QbR/7OeF7AagUif9pEYx7fMqrIsXCJKKpLV/RHIItCDYP2WO4URCaVueoAJe3M/Shj4o6efvH9pf5Q8MLM0rn5MTHWhThivqYQDwjCp1ZsPgq1VFS+gcnmwgHhj2W7XzJxiNPeRXlxI2vL+XTT/wPBYhqEP 158686346608862569574095184731081143351413141116869402750758091813874232272198082464382169470744476593016502816563462778075467588097653320101723165887488327616477297401486647183409348122990505635004320879840358339260797834264972100385692477324858942142372580281421734058008608134075577990829273447077276721423 +ANDDgNXOB/rXwmS4KEjiHj7RCDocVrMv5SU0aw6AJzNTBfseFngqidXx2AJKOEeG7RDDN2gzn4K4qJktF0AIPG2JbELlLUu0MFlpOLxamp586qyp67Cl9OuPq3UZTyQhIsSIE3VQkvxuQkGsaV1owDV3BKIWQbQEqMQI3yT4ELHm 146598844784260148346676185962272439320781765598895126402049215152385925250917998794921584290777625240122575975327405909800121511343265147922400813488099624745229653124857224399973509428158163452130086943873214460600035260925149630502192183407327427517292065083168010281295559088633086659209316582810260124134 +Vprr6oBnWuxIzyTZjuxlKSdZhBc0upeNBHVIlXpQEnN1Q+XURKzp4/6Vg/koITftr3SMSgGpE7LkrERMGFgYaqM5XZ1RXYFKT9dRJnz9VRDITVZtdkDrU04bqo2Ur+jvZhvg/oHBDTgQ4nPLJfHO3+GEmUtck+g/wOVozMMgufY= 60816213163057201559480662231646403262735082707152897397414589876256824040344252799972529759737904461369360580708093117244392116003622336721789703580184437841209963565058475060017600871779929808204093448248984201640754565635410002090180110910120481044515630478472999135146756643143415057403006410330361346550 +do4LGsm0afQLHl9alWF2RVyEKPxLIErsf4pTPgScRE7ZiTSVErbCDeyzd/KHzhBLQs/DhHHcw+OXj541cIRm6jaLVKiT8EwLW/dVG0AkVli83sFh2f56Kk+bCGSKvfGEQcGLY2k7nQ06zoMlYR/xbZCka6Q6kSq4YBDQgigQ1lU= 83252051731120517035090523892596419800592471447735288551342681962005778435125655090199060145942826521644585427683714084736143440310518046334877897672493531918539106001203807757254797471481884534543367685912500572052457610702790097953420236852480969038388056545966568595395722585797418296411673622376893961813 +OL2Qoj4xkqRrQmuuLwrABG3BMMBNGjfBtVBNTdBf7g027Ghkk/z3aK3jKT1EPpdiOdn8zXYBSO1mTRGyK3n7Jo8ICOcnlBOF6cZtDsb9bvSVE26MOD2wzl6irU7vzS+s3vGBkN3AazrxPD4czk3xezA9y13DJVnNzgAgIQHEols= 39844525812817530522650122383059885756573694015271773938493414420875846359054562126060762455794481186614035892021706051863945033061233991184379580556219478200155757966121832613842937722944431875100059046588723473670448006803481527981834627086055642349130254917244469014754132003347635357123155857820000494171 +Ljgn+3Hcg5DOf6usRumk7P+ZrdTBRmo968HdZU1mS7LwLW3Hii2KNkwMV7J77zA0P1pnvhMSEEeh1RbCUjLtSIbt3RIcOEoc+aO0eINF8r99l83xF57CBI3MDA3AAbtaYATy/NUXSC2h4W5kdsQuR88139MFi5y8E5njqxHu3UI= 32456338403763561215581247445990611953939298888251578685087656354454727113846722731945605696397627662593375001096230320486703167389461057538581895745078593206660798580358701927596287363374862536765135996838944212622199018632046955402325290145163082309469649329852148345837780541107029165352782710901375425858 +AMt5/u+ZUNm+Xsucr4RQPUu6ExAOq/Jbcjm/Kb2YIAaEQ1czIL82wsu6YmpHcfMaxLjY+EnaaF+eCWQPeGd1av919+QFbQPeh5DT7ZT9klK7BFyVsN0nEDJQ3AMMJqq6lm4sUeVxDVTmMypYnkzRl7jqzyCRY1MHA+o2LyMECdOg 142886089970163885609957244378225169093559131065687633458877059657380607541767850701139140472705242750285722732461954100519608059127637509286558848391554697942686619832870045594188204522385787253648018847569919409782188708374165437385572046835539379151066214153911415525465041951116179326632238059135825466272 +AMvXeHCaa+zk5VdB27KoS8XpjSUngaw7Gwlq6e2RrkEOxBhU2rGWGJ3fhq1HBNRxDf0quqfYTMd1speisaEr3cIyx9BhYwB6A+Nex/Sf9DSixezhcgEz6c5CfwUYP0QTTOiZDqzz+GcjKikjN7DKJTO0WSXMRG8qX8FBbH0rlc9l 143142496664357119491819741364830737485524654099662921673419335301323845847085335210884201567922636945282124120681371777665458057821603161276185071778040317947168899788341482064834489328957963447735297898161379277478278414388733161844053774747425459239004132791029364174047523473372650441001639174571312926565 +AMxoMXHfE2i4khsAkv/lPtLQhbWUjP3kxYmlJkpacpicBB6z/TmG5zjmTC/sqzBvBn3J4UvMzKYFyk9/l7Wnuc480500a3S4HRVtMtirPueV8v/SPktL67eN2zoj1VZA/Rex0aRGjW2CzEKGwEn3G2bZSgdT8hKv7AypF69ppjz6 143539479941314279463880342636704987025205547180882175105616955926182352311179043850344463145750154442573797875223178075233807385237935671604701513551125937539235111702655902037518920150424691586943553275517626347557879039695678271564616114192941679606063184290901862703975921261779714258077775731727612132602 +ODvOKg7l9RCn5CePG1FfMitkR5l9+7JK67eU+WeA5p1YXCcKS8GbYAKCtXPD2QfxmQcrNYfAc6Yb/kksaq29oW7MzZuTDzK0HXY5xBc/fJzEuvU51gaI0PR3cuU1qRlLqwmIlyt16gto+2E64BgPgIKJcAjx+TfH/EqNeJ77/W4= 39488587053253042573878502921384752550143716864908041972426777545317969264945056510991363961916339225192727727267483337259701961148978214005913510275048195308792987888118270387288989623193626554910652030960235845935461155296845475356011099372367616732243132816329531758943935324760665826550992788664237161838 +AKkznyQtB+PGvbVroM5nUIzhJUjiNj7q4fC9sSFbmDgvehnwPElVlie6PimH2FKonGV4GSaxZ+osil+9omfkb4rO3pq8fy5KcFSw/gs09X/U2eEEcUt/4oSbjs2NaMIxQftM2CauULiwfkWdkMFTBkHnh7Bbyocc8dtmrBDdoI8a 118817437232756222334188081193205110010964766506378146125932730686679941224328135190204402802650523704343176483564284220367074983943319572348376466341132480772885833789613392397284313483009178508647973749522358005819092779831781339778163122774381387989185969990310049504391258988402795259963134610905036263194 +AJfwWA7XnYbTjlJt+9hO/Q/OubHkUkyMYrN6Jd0cN5MG9Rg8W3i8U6oJxT18p4XozkiOgPlF1lE7hIAW9KRKJKGTue+iw0okLq5UNMu2Ha6l5/wzKi0QzRVTBnQm2zjPlQpgUorBBty5mcbt/B/Y3vOE4I3iVXklVtjQ7zIBHaNK 106695084438708194568048926154027115609888551145480521213711726807296356271397749432698558860759334362315257102647885062353922543502466463770991058956633500180245599467233361812610650830611712448187310827443315947425061886163301613989593906515923245020641415290300558869209909418659128196109640872398602216266 +aCXItk5XhuNrbrqJr1Qm04U4y4AzSKDMms11PgVcdf5fCGdizibh6/oZqx5OitM26nRz2vob8F+ZIP0CIyIJU0T1M50dVTbbpwuVNdv/XI6gHekQt0d2g34x1TQJIcsT1VWwGWTPNMtht1hezBAYxwv105AGKnqdLiz04YAdEk0= 73134927546833985031652237686088635686032103401394612286045377544136784429757461671691980910279873140130943470029643791712859175007885735170485461366406852784845528918253441791024065848540598601036357817496637108534035807393364939272891745520961269029038360205258229770737579266643408540634722493263322616397 +APNeoaWlyNa554OtHP8F7GAY5V9F7LMoF2ssg5wBmsgGFktrRH1C4FdyD0COrzIb0Vcko1/HiTnA9JXlfGKc3gTHEnO0gxBSDjK41L+EIgUfR0EhAD9iftTaCoBM7qZN3R1MYrSz3sevQZNMFOOnRrzwWEXnJaPKAZXvsqPzOIF9 170899982929163229592439208307232242235219591108657660041403142612622997092685093132858257827585941687488772925553142105567685213341947938835403410054637382864108739466539574004149772568683507025358331323655651148107044968424043673850583150424463706583215452211942132017052425497789362680979074312857823248765 +ALhwBfBYpOk1pfJcNut0C2fEAd4hhYU03/ZQBqVe/7MgpEDjro7oMvSdba5kjH/VBssmZVqpvuZ5lG+vI9lXLukhwRKJg7m67HG8lZXvjDmjU/PCjxBPNt5r8/DziETYmMa+fhaMTw4hedZcwDe37t1VPIflvM94sBKu6be9yJAn 129516480651398210587505113546142851617282590236388547627336279692965778911450075230961856270046942312918567973875005814982283590898552829322178788678196583244198944578081007477482775130405341039067711963061287597331433268366003672643052056973656674139309732186091974604170508497340243515339072325943686631463 +c9vpoiZvtnj71b8XguD67WayOF57QgOX4V4L++nG2u/RY9VT2+0tJ/C4NIawVa7ScQZAPVLuhV4J50HJX7FZgtY5n+lwMzNo0av7i0IqTS+1BBO8eNJy2wkCbWWBxNybuNnF6OK7eXdPb2Mmwm2OmhN2/j7HAr0cD7rK/Hnif7I= 81358980280155473712258342299472964374474635149963153129588784719499494479288254287754874893180126149146558961101860327826747785201363745989346818037655063262173536227595206355647880155693272153902647256175878517626925488264893732295267833614283963802283320574654949992393798458265266551024756663538388467634 +APArEXNLzDydcHrieLDReJryWxFzcsN1dxjpJIVGeJp6itsJOrUtnmXVnETtaZhWsmN3/Zh0R7TgJ253f7PZ/Z2xCEdqF0hs2MmnERSywdWZQ0a0McbDUUaDjBNYFht1wvS6djbI1b8RfayrnEZ0miYdzrrP1ntU+5cM1QBAvj6T 168651870043094856205824264282870999215855903395882323164614939540734011037112413507417141209480771157672307388419164831992909066097194364645695794831939514470650008210390333649278806163193463937050083854756730458780288720541495880958909249273048328511615821480782977316719631334570687241232556472064072892051 +RhGyx6xibf0OvY1XjnmX5na3G7emG8PWbvEa1kIjR6pK6K1MrMZnxFefXpHWInFS7ADESNI9LHjZB8VW5QrjRVPMksgdEAlkhY7MyQxaclUlShFl2AfKYBfIIro+vg7mUMzMctD+07BLk+jejRHtPVIxHmNnZrZYds80ve5z3Xw= 49204219353786910100605282012781696579642953908541693903348594981245301165936599174304121350092894937817100350990938057159324959104937469442065996667276651025661016077514839755853073999975805394464570132481314896694678249282338429544941873047382467276103868995474424700207571657816852575364781281563515280764 +AKbFfU3GL6NILVyONPVD/X0tffk5HS//7FBp7n6JKMXu3VXvWnfTl32R0WyVHk2yP0iIyi6SUusSicOH9ncO8KJHmaoMGN9Fn+Zq94FTFqZne5NxHmCtwRAbFNDVGg4FeemGXEe1S5Kk1VcvWqnp+QgY0uwa7RtT8C7/T+1pZlwq 117110890075563714812929271250884717870581483065920538069845585667296154465072587148155060755111295509684258790280104272121160614620669593483929827848744548171793187278583947500205314283462739235860439216105116687015890394925743036369717346234391524403038196640934551590543386844279091801685432977718405127210 +AJ0xZ9dfRc6P4W31bMHBymgOq+38ETEIMvMtr+wB5WTcsquZY1IUB4IVkrHaOo3W2SIr479IfJOOQhmvyRS4iB05yDI88Z/fJfXarkH53gDivECuo+5+JmV7e0S6gCvOuVamwoQjlK3G32bCV2946ry4EyIsVZ6Alk9xk7X5HfGU 110384671994603894282707302829898242894456931176497230904862171369974466400767175784681299142670706023468915238955836087425993929524341269289746060546848852729416925808186253355106621584826213979718185296723694190658548757311188764342751280681935289121682174507629679900374674992438818324999211250580434317716 +fjzmb1D+YBU5Wn1GlwhxjiJS07k+fXxjeNRbOv5SjktzxOXmautO8xZ5ACOlYrTt5G2gzW2PU6sYNfByQ0xoUSyutOuQlD2r+8MnDrxCo6RxT3P0dUSX7q0IVj+oLK4GPbscnKLfe6KqUcYLMgKnDYnc+ztFD+csL6BQnM9WMLk= 88647261832601702291191332432291274285041869480562430895152086741320122435409959711452438332192792226899741738806447713240934608106883094466050154088410020909933636902495700779087737304255058561688767369900548260278700135161077055869478387490726087630962098228537973426295306997128615315548440548541717688505 +YDg99aHkQSh9RjytWknbXzcgLD8MrWUEHF46yQLHYANKXaQYyf3yGM9TYPCDUqWbOapqQe+XfOCoACLyRg7vVDsnOPRDI9ZFUgCQBNG06ZOxzktEhnNJoRC99da8jyodFqqk2f9UD1lVa8tsQdatjUDocwgJaDAOpYEyGnUlbXo= 67567767932654827067250684965667741848878457020992905661955722020937161710030993261011062929936964216357930453809610708591260182295097124272956485574313839759737390934220465669626974544253750900911093325004172643146669082793591441922014060981070503803266774197958528843445580649512373693546027107823355522426 +ANdsfO+cNtWsbT/QJHGkYAL2WCHWVPrX6oEz78pO8lUwiigVEow5roLI5Tm7GP7XffjF95z5WDxzpoam+Bfp4za75D6ZEHQmuFnpWQAmNLUHdKUE6UcsWN1rbV1uY+x+Nr5Vni/M7PfQi1yRTTJTYav40tFPb9rY48FsUotivoxd 151275723772668372472508916060743043308364940375633847663054782759325087560768667906829087958412643723335046123025802453213225972572697773468957759328009026531148112732519692142632237595562259864125679649273054426879080697360204352423668940795473103047320116317252295126635024518179060076282921965794883439709 +D2Z8YA0G/vzEVVQ6itLPUC92r9n9FKRpf6lDPWIgpZOOfIkukPp7zzTlo9Ej5IsBrZBbtGz/eYmlHeZ8Y9pQj8HFW24HeKYqjmR0ujbNxI0QgoE+VUwPVg0HhoQsOGmq47zpXpkDwpOAZbMh/t1Bafq6r2zM0qmiwOacJ8KFUas= 10814483230552506566705634583020057064935800294861277580077052473134972003523900930560478187758928889017740705417070994563709463926267126567504805864719383185267204810142444719634360655595490833208838383875687102074846353850310954150927702228780599083427768247170427544730791038729428517279760042619935478187 +XoZpSMHqlOyPYJS7dWSRNDJHCkjbo6+DECzu0FpB9O8bftcxan/06Twbo5d1lEqPlLx3w0XeWtrmCSCaeVcXVtlY3QuPjdKPv8LBnnhslPOVcbGyflaTPXU+ITWE6rwnIF+yWQl3NIwCV4EBtCT+3U//Dt/Ebif9gzfKpKltD6U= 66377743237695515693282032069691369056215169443985727092982918806809030742478033317158686828712146024066618073633406428345129492010236994055590530566431286733776441810601990431112187030942086686719669823512292071202675269428014136307286941704297995292544712278047959299939833088742083527714893795660235870117 +QUbbkyJQ0Nru9c/nPbphM6VxHp5DWlai6407KIDbTGvUReVYI7de1gO/BFphL9GA7gDareYoMuej3/SVp8lEujXywtXzjiI+j2TzR3YYiMBAMhsJO1wU9pxy69Cj5xeFFlrOycjE9sPS9nrqnEEEFNPiK/GDDTHj0KuNbWSCLrI= 45838919357034925862751142472777409057791233610959872523563363744902783251621354580995921495295078179996083468819097423327554678806691589090814275138081407920379810144694354354954459732280968086760894209634364189264517251735804373673532012530665557440070501687207620525228416650281363557992436992284712644274 +F+uI7ARCeAlnPLO1YR7RJj8LyhtE/EJMcY45lsNMff0YeENe8KOITZVxNA55FcxDYpg9sKi1UV3/ASqkqpH8MOxWpBdT2UwSX3oBkp6ETfJKqiag0C4MS8cQVsfcKF39BJ6KUE7X6KUEj11j2YIIRREmLPyZ0LatG7dN7Rmv2iI= 16797235966984072293396362937533957334369977688369659112225970370748312376722010874726300554329794854683394163379447263409228872034356195791733533528404245739693397078461712458035888813157166614479153484688995068722288153129390850561042173295997770817893349738328312152341860704179681230323810266038959856162 +ALkEoXznA7BJlBIfA3Avl9kygQcxexEMApwduVRiXeYG0uEXMQU4rgMJBlPqs+ly8LTIcLFaLnJAG2KFQn2GXz2TNa7w4xkegkrslIJEtBWX/lc7VzRtcLbhaXEs0Ci1ValnW9Up7dYOj3Qw9eNo/9M9b1fD9TI+0QXFtp1ge728 129924120553920201168632484268654219915712271781591182777925696006023100660478316445751842982460082888615429513674356810187315558964251402722465707617058251479494744427428152566665405423424700027316505872162698141109433045594670140335040479559124757490095995568556894332243767736124299898808796118800328801724 +Ki3FNTEE870E9GaNtbT418CLSmf++s6Di3hzAy8NgiDOFo+uuicJa54V3JNRxOBc99sl/chfZuaBQt14BFOQ0i+9rm2KD82okNABd+SNfXOb0Ow2taZX8CpkVJYDyphFPyHbPIKmzwMShNx9X2z9w4++tJgzBzGcFTPv1nhAlxc= 29618953883711174042338818332957726953262658484143534778541769862244883781157097499904047532839425875312731531093860721544220959674634750905085721866390609141599426547378130082409488797303960018348798930232014390380383063108812922828160584483043190739354817699497573863286563890071313017508437166939160221463 +AJq8tcSnAq6M32ViO4hVGiHY7Tb08cLVyxpl/v0Y5adYblvjrbsFcCmsNDi5PnBOBl5awR7KZdQ1xgq6jIs+SQbccEMvJvGUZW5MgcHrXBj9XVd+8oB0z0eahqXpgYBqLDeHLU6238xR3dJYFf+Xrcrzjg8swx66OmQKkAQVJtdq 108660120968150664552423780971948386965268856900017812123107864829782135741514930439461240950044759098603910762272795612101834680870627850178371693837566833495418727543557712057554231215186486008080050486837716071537742708913279026303380104388546316647349432118287628353129105425052237438199445863950767806314 +AI3mfrgcRwtE3mA12gSoQV1xyIGy/YA4pCCvja4mTjvzQOAfiZL0efadxZH5awohCC1SpZDCFsE9yYp4LugHKu/A8zMcp4k5ena8sTPDkSod1yucjybgmVJ5h17Pru28AzHQ/YUmCnojQv55aV2+AUhxzIfojY+NT2PKRqr+vuf+ 99645829268436288676280252226747461064597487404802430565833102291706103139410465131373666856042539909746769688396958963177805479987372681967013633920910376342526433530508868114301205524789149997372160919406352823342811006288909548557622230243808373083272214426118230701324879006645047374853535922112549545982 +TmXQ+D8XFKSclXwnTIH8d+sb1IV0gfm7GagJahaFL6A9rvYaZ0NTizkG5DQ0RmXyo0wPmLork/296whsdNdUxVAwnGFlWWvMV0ftR1fOvN9KoT0WtVZ4Rmu6Fuc7q1PskAZzIp7MkOAxILO4iX5dNuVC+GLZYIbpTel3Ga8fXuU= 55052751096768041533898435453266875315629605001878362193939750978427494147944918632414581744895066623527980497732722163665712245580312596487741856071020477624754815927936394948233480228964159047139170955663289543349257377302556035170334384320502468579367401821986660515827461352578142560630318492817238744805 +EF6KIBWQiQoHOnBdJs1p+WIcAv9ILt0cnQVo+o/2niOtI0C+eFBSiNgeddhotkQFgHvGUjq8BPYgtLC8A5IFKGzXu4SYj5ziagka0hqfhVs9zVHKNx2NUoMhPDG5R7+giwEGGPOayGHVNbsBf1FBYG91+mwy8hnNbhcHSnvLGk4= 11494909948912248031301686864833544028186348338729984264372557659364976118965740281229664413031002362633393381744365783802034700038490736736266032000546393704814403638058993380993275865674190555703046732456017652317200288968188655019374159412919163798248766655991273308390043613040731449231289437754791500366 +AL7wCh8tkFe07qChFAzRkrnNehvda/Teroj65X1Bmcr14+/zeJlZDObYRYBOm8YYSYNgJekcL3o9lLFE34sCMbSJgm4dGwpEVexiLVi+zc8ndnqBDSAnRqtC+3jbInm/v8l6cUvuzrUNtzXIQ/H4FrmPMiVy0EMerkMtkfw5GBsd 134080980697158076909534078193319899756347955848461100874771253577754225619652121295523443912922220564492468474647193062555347746840044705102003079330399499915801536721237211615317000955332058281901995149084303143543150689010335818219129745452688372571010816270728441637278434982752674030696337642893239393053 +APunLhlblRi3bbRBwSV8dsw8h5SvT8ncAmXPnca+e1dLzrQZzL7P2OhFope0mW1MCDl2kJPiGTdK3SiYJVsAFeR3r/0z96g3oq+8uS66T6VaJym0QToMsqQF4/fUMaTo9HsukyPyOgjVIU+6TiFd3SxQKIu1/GpQWVQIP2pkHFKM 176716779397275986910036615967409090183531310366246043951791503601618945774743601662530806467045971394247287367421508126613573039423674729894091424105133906122821596079925540513892022311039293333114333317886304014722168786051080135090242879622144693440448171583324154550086458411590240882982297314605229953676 +MM6B5AgdJKe5OLlPzcXwi9WhqQjx5KsnBYxxa3kWdGNTdk/IN6TVd4Ptn8lWkLm78mw3DXP4Ol1sQbIfkHRoKFUN6TaWg5aDCJBDXyHSTZI2FDc1di0Te1SwziYn0sIOe+R+rfuLuHlcT1xaZBgL6+dDLAZaZza36UEjn5i/pTs= 34273208848307582992498656582721015257885595139328466874135636009184357438445251703533153492315835793684794951576799764181908090765379592683793969576893243386892292517067596035059342970830813419330530731370385186653239446376170533147020072285887964430731437765184844167400169982662183791828762458682426369339 +AJK1dx77ZA4F0sYCgRL1LKSTvjGTKBHd4QBeVnE6FKJxIow82puqtsVZ7TBxbECex+LkLQPrEbuQaVr3giUDjg0aJCE0D9ZVXCUS06qulqcCCdWgGFHXDOQzTWDn6TlJCGxtTEMbMxSlUq1q0iKZ19kwMHiT3GydBn8/G7tIYd23 103022457217861194294329435482792508957642944252832971366936865663608381648431732294396977429863681671686490913575377744795372643599438468695483808375208871881849232129651519218503507811863794426234594709451104684234156597418383183271923307418704786548452806494411689822939919114966188329657999811363991575991 +fPZNsqUYBbVGA2FAiglnByxGJOZkVSpj8Y4QNW5wq6o/1e/PRwp0TLYJXIoCJRs82pAj0QDpQbHl5lCZmNxEIQP8o8xI//HCPxPIdgBJmSfm3VGetrOpqEGU0KJJqK4IsjoVpAfPFMUMOpGNz9CSvCHGk1AKrtYvrTJEKmETuig= 87751387019308584846595931543798879607048239290774788042055795835726250309378365187899578817976976035304304847968410200168743967600896348021636654074952051821111673620467434295067182213181329543946368332581250062140819766061014427755090798550122401239987766844126425179573454145697756278292448630509686471208 +EmT6DUd0bxcdprYhAnycQaxm89kltJOlIOGFFRmEK90H3RhzBGr5PRVTJVqemFVpVliO1gy1nPHgqDGVNIE1GXhrhyFJU6m+HJeNcduippRe38xPCiuraRkXao79X7WAiVYUq6RIH+UIRnfTvHBgzTwjrOvKJ5853hYmGaanjh0= 12917015385266582065020051081997430892582163827812227349569911846746592973268746845211126663077128575098045461893559476227689488349263954564361736197688317585888118974603264677576027836032271531903881104937422976121352854003385726888601980526287956222142458858211589791399646989299770657341412683499692330525 +APtOYyWzdY1A/YU0SGrtjPdMZA5E50Y3hJVXppwuuSk04TjXzcbu2Sqp7sMnKYbToRW4nB5p2UnaLPhTRy0yszOd1auLngW+0ttCybD6nTcVoP65gYOwXGfSEQysqKLb1OfV8kYq5Ba92Efn+CcWWWuS0wEr97W5M/Hccx9bGu0r 176473215292413922394356058789571494026727424839036665031567966488209592078148711908841964690807374236235612412767651029865069639786447019874344449598703213025389428836803984245755885691094364960118900160737925054803955567361126391353868279642836569627177281508980029006921064654964339077608785831304875404587 +Vs6bjpYfFA1R/QTeCfhMuZLZ+Zxo6wxq1jFZpi5SBR1LaUwAtOAj38OJC8L7zmxSOj/RGEmJHkulI3E1MH7P7xlWbY468/azfot5fX9BgHrtptV6Q0dkBUg7H91+tcxdbm4/V0HGQGa2rZp+XK1rO+U/d0ki6iNbsCsCR+OeyvI= 60957991334776853645581868230398759578123373154273044785333939425321390401088800849629483265841435899835570419798325123273632247193463641611211088549152950252041797959644227170492417662363676228611376046334386877555777556575818860902071813120592757466883038430756577949025778080997296219236534786815367760626 +GiauT9A+wmwJsFbS2OPIM6ultIbU+kT2NgACn1jFAy+vNBahdfHMCH0jJdCs5TbmKTCeiEf3ITc5TV1OSvIejJ0GRkTf80nY47TAhiP1aehZvMAv59NQHHTDUE1U4TPVYKIyFpm1V1A+JBHKJzuGrB4lvqB2ed7k4m/ZD5lFLMM= 18363925023885496669420377869542744504974590667921570026763131637088916425434675950812384919000566852243714758512996458727914094904422651029609645299422563453163291342992902510788457007623888307499601267675322986672697397389663297565071582648674012080122614260400848960757021864980761735684874056409664531651 +AL/9KOZLtZu4+ZQYQsmOgbST8F4RV4N/Z+l8qsbCFlHdXHqTTkcN0chsccE/3KkVTZsAnAyJqogbAvB/RZqttaK5a8iKlOEoerUS92FVQw/42WhsVaFggR9cHVuvCD6QqclZjSBQKQzUMy0YWPWlycAZDIv96tooA+V+Fk0jbcFs 134819194171226950171930028888667967094069342154233489571728632904658607624703819928943642011918061760802468868660586005724399808048609316802502143143910585363214684061242274402109137825176291816945489430125510625857564490981683683589784133305376252294774711594646923226452625156299996630452243345104727556460 +AK5x2N/4+PKlsW/fNrw76CnE+nS76Rd7Ugo3IKhMTB/IuCc5xG4MQHo5MlWE0oVkZ+Gs4CxUpvD/WCCjHHFlSxKG4mC6ehz3NVLglBt+f1RWfPkF28JPd0UaIOG3um8kG4J3JDN48PXOPP86A0H8ZYbE5+ImmXsGAcwvScUQRInU 122499245103202714319465533564374494931278163571999934877854825659720649344163774228004853964635693562785966889622928722984134944784141208867445419597834322541679973956606275877526560988151196822256754309120410807075405427166696093800381410682490767468563176131997424692783482903880902119461752084196789357012 +ALZ12i0hqFhwRAikcoahYzH/BUolhgZ9Jz6adLvvTO4wk6LLOpNC/zCz+LjM7HazZomT1SqeYJ2X+WeGFLADHuWo+Gp/I3S0UEneYHKJxoU7OoOtE0mB0BCncLckHao/LmbpnQpS+Lx5bRsr0yE6oWNea6gbyRm/R0to74MI3/KK 128128022342420083856194424802390993133863171077961467523372211039771843125192435716337829530528063182315478279257832480290950255315151577221042903861075751839976362752440630888566422581799720709574650482021111126414843635330535518992034746102956214991673417580508389225948159518319625680855827280146399752842 +APXxvLifWgehdwdTRAJP5KrchRzgbUsyMWKcPGm2ZkwGDJjoTl2LIOOGVFiL4CyPBxahkEHf0nMxBN5oNGX/Y4W4PuOAC8gMgHzdLkPWkpnTcyoe5DD+fQsqNuKVw9nvyB15fx8k0d6b056nfFjnnRqgybby7MSllAWSKRYRdxVm 172707950911363219032118650562553641123743396229371815589867086054370029540557395298194067635069298952836929253340374819975848769009260895874615676938511747311585257140973518651959463416682165208985512233703837931718385346209362040743041262031997793519095342415901373534535662377972036003546589624834285049190 +O+9ohtZ9SzGLJoZM8IRQAjhc/GPt2X5G+M22ZidYjx9WgOTrZDXorSyxLuHxay6djsJSgjxYMj8MuanYSn/DzPWBB1Gn4cDmIsfeYuzO+vUJ4l6d0nIvBg9Iqs61/PGFd46YxhnDiVQ9HEznyTjzESnNqc0+/OkQVJcwNHAcZBg= 42087920806448980363073662127262313840530298932643042322138035915324224188032438119079107631420338701086802583985117830416851550991102672642532160807467909040086448764318690465254898516502941122327185894900817634110254371864896139724173087625913998657136384357741816102965779105122269429701537815263708996632 +VJOZmvqrqsIUTQSSJpZPhbQIYN2tsfBhAciWnfAYpwjK9/ts7OP4Qgdp6T/V2EsSRPnfZ0VKdLg1CnEWDhfcODo+/BZcUrJ0AviFAEtdeUhoMSWXtjel9Ln2guHY4s33z2cN70+e8gfjes65lCzrxUIXEF4nKxzKBnScoooQP5k= 59391682001673484862915842850714742391303140646889359425353339320546979084250010101273851580028171449840778038774656177449549941659895629203970455580974953864068394275066532699748911169800076515776388213090834432354601344176559839798153004796057709798368011673585434643656820656931921831615507416411999846297 +FRyJCOgPziO6RDHX1JgYGZRcSAuoQFIZM4niD/B0twK3l+TRpmVigKZAJnZZFtmX+0JQkDwQn3lcBGQIL6mgy+j0hD58U2/Wd6xebuHSzf4OHVGo1cYoqZLplszA+hVCoDVTHi2YAZ+GtfQEggumcNVxqfEZd6D9Nu//hm0t21M= 14824975573460749317081504809641216868382341402512168178334301409725840669112911061147252565570697788806398498723577368905065980113760265945344671897779830912242224090954834750057278285419880820811348943398148063418809729356397202526234113316098584002071850758705282845646489058224513019380757604894853946195 +dUk5LyS7mduFJlvh5o8R73kJIeeTh0Zli/y3XjtIXfCaNRf+wDlD/pX91JEwsQ5Mvj8yq/Uq13QyWhoNwsPpXVcJtJ+02wtIn5darsBDfzcD/LbWhl7zTRUeMjZ72gAWi1djx94SWjrZJS2oWZU92Og1yOyKRG+ua0AhHfYYh6g= 82361050315899968537319599868832189063658136463903643442673674137187842597528653416212822014359684261704550279153006971937114135373937934986951573613797195556144113400128502946618028800530164890707031379614952207482505803377774320259789692177752930767589642007257364960987343146063216186985472686575891023784 +AI6rejwEznR35rIPuIz0CP2aWyhRUR3unJ90YfxyuVYxrqOJQGSDTSf6SGDDw5MqpZXa9pWuwpyrb6smOq4ZtC3Er7lipJfXDjhy+0k1qcfMjmqbATUscwXGpgW+MO71cttccEz6vhbjndi8gvG5M/vfL2l1jA8nXuBd4e254dbz 100186164434910864539376019601151338080943067893748898987236087770762310617199833479771711726248130012472861788210345311298499515751355424063761182369333224929721733015910055321263016834247318907562652286587380604998130368845939290804442878127169587599285040969551065995197981341260363722618429042861484922611 +AJ5vLZX0fSs8dUSBqd5hki48T9cYuR0atxR+qv7cRu9nD1vP8uNVR8dLitg3XH0RARt3ZmOgi/AuggZt6tTxuIBg+9JhBY9WW+BLL5CnYWHC3AKMi7MQBWciLtmBpyF152bDaEcV1PXxtml2KxX0Ba0C+hGVDmJSdi8Kjd4AkfU6 111256341508463539324514225759801553679558662737345522765042612717818066374840372549356543720386819501973783940451033901079765311790026584654529398345993992144903839534037331533660672892487693477412528974248713261092693018326068480417183236210881306241164169849090833681510163753605662526243408192127670285626 +ZhXtSzn1GiFfHHnSKUYZiTcEWqlI8owyCKFjCQ+VEvkdk50m8uN7RCQ6ZhI545tN7Uy0WdLstJhgJETBYLHHIoWsJn07mgPxuyO0XsqNroICMQEOO/YWQFk1c0VqZifcohQAwJj7fONzM7hTcA22/7gVigJ3iLq178jZOJsEPQs= 71686982768953132894579286530164112027530221141251507987469672039995314435159469907420372652392376452531392493658576814100773556880394271726970628960571077839124343525055625420896355363707908511865700866168843075071778015504724409171911254647909938237551680861008772396291072284353858575645679153885560978699 +Vc8Cw5m5yI+bJ5sUJYm/F2wyZ5x3D4ydyL0uU/3eVF2ZJu55OOlC9pUyyv7WGExClHvWpR9mhMnsqCLyseLfM2Q/YXJ7cjGPKp2xd+fvwHa4hRi1FdOxs96rJnb+HUt9hTwQByXgzpnUfs7AqrqaNf4WSlBNMu0IOOqDdB4iVHU= 60256873326783629723455608618518793848697944184579877638436234491615392142659293975260290798403892159720925893207048153291000664050780029732557737984085196691225472664027706406879051455184548871511448456651238810812870905640934953489289909009741493031472382758586341375517766302753448531830002512912250459253 +QmeUn6cbpE8YrDfMETz/+KVFaK+d4NHHzcdj/MnjcmqQSLpP/XwCW/aeudlN3SfKd6rNo1XZefunZO/ek+PHEIy899WzjiJaajhf2X05fl9WuPEaMES3Yrr+ClogFNQ+9jL8+7L+J8lDuqQzvchT0U0RPay5HSNZw+ZouVCiQ18= 46630904037845609335515965570673490721137364238213103678233212262384415738654627185220187275286458759154841820256007930773120637898228224906635911124921895934056288121005350040349882413280772888907627838315559544636626856478316691755270725623680935763476199888127096014398699432042227882284223578563208692575 +ALUBYIShA4w5kRUa6iNF8S33DqaprdOWjVBnO+j9CCGtUh+NNwfpKR8AKf536MtuFFtwaQvRIlkLpaTYXuRxzyU/YG2+UfRQF3pEmXQhcMxJqFzqZ5nWCIWlJ/KtYS4lcC/B7hD2UGAktnIdjVUTSxX60VzA+zxeunV2iBZXQlEs 127106299687401374061881872616647348819431126560557369258073443762502337592227172639640997680536372567116568811258505773087926491911004324918919511363985868314578663758269650473780772688462266790559846182685481907703974916356209771821075179827563487466641669110315430790405454641953880582274165368514679034156 +ANyAdMnVCVjmUZGiVdyvGE5mUQpKoJOJINqMAfzVUGvvxXFmGdoAx+xsDRNAP4KoijpXk6E3yPBPBZEWyhiHnyjEkktK/gX6gnb745afS0QIlsjhKCk/W/BHXkzC862Llnc1ZGAIsERnGceEoZHdICfDUh/7nMFp5WuSMzPB7nEO 154841617115465511611746667401422322067517612306328612547616471923266281876818466022676728696273611923942543658633762267658490816264271663863494188027433799849037906883352478212451733963905925106470599843045599411842850386623187980045961158399934160107237440980574028985561404965317132715808604373199725949198 +AJ4nfhDe+HojR2YrprDHW9FVUxsZvoIekwlNL2iKFRFcTB9IcEdh6QnGcaRinev7yEYUsL6saSxUj39uWlqo8udJFdszuuQUmnloIi34L5uj0m1OpLy2dawpFQr8pqyA7go4ugMMj6XCtiVnISUcK8wjHgY3Jed/EKK8k5ce0Jxt 111059703393618496515021583605572584329116596402705082562306930876194742195701060137568030171429700588269665205795898835699633817098262654446852249498668467827435829513531633390969638488553144849154126899372953755511962841193763362947708260103832329116485114451074371844037650417731807385491783373627950406765 +AL+heSTflb2MkRYFTKghfzqlVQ1oE5vcx0eCIsy9NJ2NGFXCRRvoGDVoB8UEsUWIRnaA+MIpwDKGpbOS8kRQrvBvPe/xM/t3jrGkaS6pN064+bCBx8Y/Jq31ZXNG8oUol+y1Eo6fkUKNl4EOetmZWK8VmhVwol5YngDffj4Q8ned 134567692290185631768518572983694048149859804864902017394351513816079806629664302312927579302025923096596995134868068794900003728293470554490807959649153000914807604036531509869958441069678002226922395630284261949256022972967357884468325217602330254290548618134453007903724438628204981673400911693835033278365 +AI272d2sbYIi637kHZC+6lievgcDvT5VKaCnus3fHwm2vfao7oYu31P4st9DlqPWJ635X6QtLkU5HgvVSy66MDj2fcOfwVL09ffkZYnoGNdhMADVgOq62Ro5cCpOdw8Ko0cCyVpVIaSysPuqY7kiClf9GTdyZz/uYHDgwWeNrc4R 99528854246023003959943182132914587584844397870416002887630245681136432049666385367430032197518895755482367603560037194955739661569172773017279832774100155646116233705958563163070414171045438199561777058338188494271322834524386565519620661180246416329082614115142485663975718653564590519408413408765689056785 +AN9S8vPzo4SkyKsk07nfyD0um1riJzRqqWF9KCL+kWMHajurgPACikYzu61tL7l1mNEaIU16Ndz541o+y76DgsTLYszu4KXUOEt1Gu3eHy05Fq18zCDlNesSVjkZjPmuJr2ku+p0cP0TLLMn7/KuVOm4GlEVc6OvBNZuEzRriSYZ 156823459768092337875922818543729136404805918580285507923139232733465414368775678369646914249412830351437211620056021568154043505276475345347569200977945836210758870414054407438380975491139001471954448623922841964684437333066353208837709613982022690623722155151315252634380695513434502419141555410441456920089 +AMc5H8kywLgiT4zz5xgoI90jejsHorbqUGtBeX9wke7zyvEKyWxRKScZwzRbinjDZzN48eg/30qTZOV2Rw97JFg+EA63iZ0vqfF8jErIt3hODniKX8zayCuNmiSb5kiZL0UDU1SNh8ER4m6o5vshBKkmqs0PeozfCGQtR3bZXlx4 139899247405256530335276706333424670310599977544642091674186635734421385499036688803073040921114325725234673132788498809189814711681909865484671959982394306416477300458309408833281654917008031099378445580498219376391819745965887864647387211647794422908411100892195529730435423964537342228510107659017578765432 +AKv+3H/TruTX3wdMWnLzD05em8u/QMl6lCHT4VkK+uZwBXoLeji54Tcs/hZIhj0Bdj0URrRt+7JdGSTy4Sr986AtVFxBJZA3lT+JT4JSrq3oY1Tv+tX/yg8ZodQmbpQyyfaFg3BgeHNmsUoCrdqhj4IwBqEXoOBRIXnzaTuqqSEw 120779384043726135670909127168686589868907326577918074234323699599475436892003731971700278391108690400460261929381703781833059801757700386671579819341589048987186473249926590758009001670959004477454905417357202448886738669226760846888369186457452643053236389556969071303251275912453385963613554945645058007344 +ANXIB+HxOyJd3YYsscMpqZpi/eYjZi5q6A0MohU4BiWEJK/E4uIObLJDH5yd4ng+hn7UMhc+R/AxG88hIdOc5NyG/QyFs95ZLUC26F9rkRifu2CBkgqR5EQi2cgwC8jGxQOkC62YND6cAn/ILsKTYaH0iavtO9Tz04vQp9Ypc82H 150122383481070201614242107655752525590609186454390549085509458064289390813495886095936526832230958746095739308601699615024239939948911472291507190108935262129646691795733786714291498653838550751365834947465294261687773081563139416397262227609481906371677917295227469553787085145970923979142676551778927103367 +ZQLFoW+dJ7vrHdMlcLRGKY6T6PZKnE2L3NjXymS/55my2CDBLdDf3oXwLlRjVt9KnEiXyQzLhyY2PrFA4k3N/3P5lVDLHero5c36TMshbHgbIKRGN2CGWPEFeQ4j040IwVbQCPJeuF3jL5ikCxWZFXfeEnTL6TqumLfD9yLQfKA= 70932215714423143395949105745758445705072524008235214324766464113352968998429901322485575506330607802260244612268338586532462314021433435523464635419846126736185176246740838082062856583684393425704173881940108783636582561707441482446854068022535943408999200681879161519209676205165680598258447492092651404448 +LzzvPw0FdtM2G/RRiqoajJiIH+Lw3jpL4H+08yOpp1bNITR2Aq0beu2nP0H4o2Z1/FNr2hzuGakkAhVbmmRXc8keoOkeaAQAP/8OYxHpjrqou3WPWaKx+vUCTSqVYYf8gnVKpAAC2cD+3lW+/ZJ538o+c0ovbUKNu1u1j1OBtA0= 33171669664542509840621265032202455391098253465550501094201777336478104142847268103467889435377685359857979277521589539506627375165485879405453566052091202280471235979376217319335800766353336252760793484157724210008639813552207624049019149744883918494762511376489708611103181576211531366514802868659603747853 +APrGj1lIIlxA57DNh+bTEAFbJK2Y2P3MxLShb4fPx2aY6j88k3umoe07ISQLf9PzNPeml4/0I3w0KNd2x4s9KHbj7NsIT64lhO6eQSEteqZXZGXUYUyNzhrTbAjt+Q9LVKItQhsTkTW2HTQ5RQZfGrkL118b/I18J4P+T8CGZdDz 176100632478477421621142147788721746818712752858710594712903769452749028606541677227413333567013253138397373757811889654342173021761934591400685421771460440213093509170325205622261487145789848227404883040799927313402244625239515162996390018403365063394514244196976794479529075569412676472840544017222373593331 +Fvcl/LemWk29I5LCjU1QedTjGlkvFF/kZXNkRJv+vNZ7qgq6pX8WB9yVkk6AoclDYAhCRfKTKuEpR23iafVuHpprPfNXcqBH8n01kq3U27xqIy2hS+D6BRBK67PQaekq31EB0aOcEb/DuNaXakS9+mtTMx6BKt+WoEY+NkzHK6c= 16126868736093163702771491576570380743773057522016869811780571865928979861357811080042796140032050364543242385458140594532945509386155523162799601656485075247603490060565663264947465987286983338572455184901756399862440455644131755848583379822279676555143231305246033911608913609591095831135803702269767527335 +AKW8tvaB8YZ7J5W2lmquBniJzUhRfqFdPZPqvBoMzR4cRh1CMNdSFsYsnsaF3KolNzogdsxFpHAaEMG6zSvpNJAoi4nixCqb5SETXrSLASXvNjI9MvCoE2JCRq7kMbjPL7cem+mBPWZITGUI6KVlJPLxQngHYSFxukqlx7jznwJH 116384596458828069344020651216200368975621068920641012055593076864629080375946542748377736186556382088448816531408136815533164209947323588157210859294774679831647934533061547276394884474877353537242203645373945111105805934070657589374883764420038511061919092743520704686962593876316976299391579463759429567047 +D5N2P4FrqDf7/2Z2BJsqah4SjUtolic/yNqdNzvNEogDKZKAJyGq4zhnHvkYXkEm2ueU/FDPJRqisszG0oULdU6c7p8acirEwsGLVh4RamnFRgmQSK1vbiYB3bR+P+iFX/bZ+TWjN2Y3YMa5UB//I6Zb5kEIjmTpjY2LEPI1e6s= 10937855369372570149476727082965401421189236366492771695094788039313362971972373068736123833330006002198346944149230147444718818161877123407713821100752433128205189334393732633989950841577315682292180735057952587083688644195300641998709155269462601925653013312848413290208844194513502358901613104779186502571 +V/A1ktS0xrcwlI8xrYqvlLCFYrdVp8tEzZaZ9iNNpPH/pzVsA0WbnnUeHbdilkje+4OdoX9C4U2xaOuWOfvqLR0c7GeCkSffCqyf4ZsBmjy/BQL6rCpxMF0gIHXO5O8aJ1h17hy9LTuNzWm4zVh4pNFuHC9L6nAcf92udMiIQzk= 61752386563628388546439207444896778638632243226541303179646524864765343154194512297447627825411023405896612559648434895675553567405277169056807223959390559391191382555701580549902639604424290133917402316755076644943742815711432111554988540913643347167948778404861099845961151998728662878854088239266688156473 +APoPgEKA0/r1FYmt/Iso6ChYK6dDU62Y+vH5h/LVE00biBYG1f7aL3GdllUTN+XQSHpqlDw8CD+9xojwZIMfgpgjOwLbbe7Aso460zLrg3R8aHBpbVt8iZUgjACwPYr5UyKbFzIAWaXcnYYQ+tCO9aDIuOz+/7eIF62C81zXFJVZ 175598490446477604563905754135475294999639698464908622773037381109011373179895295130424828038708319325919451724985361900259676699137657615076219968061941008972496322083528922054390781811699677037439989404270415929836486610353098273115864435328533577114470407444852521009919911888840405368858409835197558461785 +cL54ymLJhRx3U20Y9aUTIsXy9Ags+XHy4qk3F7uJyO46eiXSL7VrrR9vTQXAbETbu1YiVWfslsPht810eUDUVaVir6yLnXkywn46Ci42FEvVoTEFjO22uYcCh8nqB8H589w/+lVSlNrcILugwfdfCvK1iZzVimOO6l3qzfXToOU= 79171550718114578361958369278761819285111811576818442980166457146638966315793211967882077899426611721874954146020093740153495693185472340728106727284441726113022873005252623222594060645383105757498856463065370975867121188445567981809371870213273555432308279508351518168027875538720367440153667708369625129189 +QdQN4qW2QZq8/fmSaqlRiPSoDbhmF0oYjaY29HcKYGHdlOH0AMJb+RUIq1aszvVtjh7AYay2TNhaZMWQ6Qi3c42SNk3A1MVknT6zqiRCGjNFfxf/matbRLbTFQF832MAId708vrFLF/o2HpekMkc5hcHB6bkUUhEI1NLcMXwGck= 46226230186280253581676626651942823886592433541360244612432763620730826574920825070086312767146345247802570752482654580909236388357139147786783758670999083804670979821212991224400629053427330483809790366665043598754931511997925850227997764381723288657884346974360232490075739442406431704368767588177525348809 +cxHvCK/dyVDvaqCCQyLeaiBGA36mV5el+1lc2eUTkHGUzX5gU0QQCEp+iSXNJhIOON8VFpKOFsziuV0Z+3cegWRw/VnxnjXcBh6IDKdupzOPB+Yl8MA1ti/GrQjLC6ikcNYNjQT0ZThL7KTqEvvZJH68WYmD0IuK26swjNGIGaI= 80804939616399473443737611589382762718815989847332356984276911837267997590368701684135326680567847542004499684038240485603420973682522792156533112356849436451918522884749244246467852622918805139990256619014116276456718693703261686778030658826952213058982142604346352178078750879100976710761147710018148637090 +AIQ3OIZevkYoRGBmsFaXJobSfLeInuKKReVYNjP5VEPoMq0mXTltY6l09/rQ3d1JjsMD1PfA7emhxex+H9t3leBIfCi6Ux34GQEjXWpQc4awuiy9tbR077HaJyecvb8Qy1FTnOHoH5C043QJzrKYT/sFXjgB60piI8Y0R/hwxO4r 92845026347218330987427785323244729176754623818531419911990153715676845614711324345879159989637824921793015074978358052562420379797956750450245721653716740651389924718711940869162230097839047895842495414221110468446944827052871968998907462191349838598297775847512250220907563815783358238473966349820476321323 +LoG6ib5lUh57rdmSkZSWzBoudytFohS4uoU/uly6OaQDOi34GeNVxu/yr6RszJyL9JWkGNgFaBIv/HirH5zA9VQAL/6kpL93a0/GQ/nuHkHy3GWZPF/2+yJ0PfazQ40fWhHZfRxBngWslbguFPjj1XaJ37YzpQAYb/+QcUai9ic= 32658152290878644668906121702816147999633088014476055330179597550087921141413344679134407016170035735846077181424615228657687216737432274043674411132745299610950657139041836412322040866250189120286839287690983293111362228893996267791120043532014262644480689231457941173330523718758287779526551822788227954215 +AKu2jgOQCCfYZ3CLkXEH44aO4TtwMPeK/eq4FtNj9HZ9FxT0LLNJh0ZXPOaPJjgznvIw5C7/hNm7rUs1JeV8I8dj3nbS3EVERQz1gc/ckYB3H1bViWREOD5+TScDusi86YO/z4ar3dauKkg5kT1kKDuU/OP5kNMWvtJjHc4Vd3L3 120581042599355202025471829872601846477331097842315143148145881424071317426176264583672725691485724160094190478865850305422057632110749683552966861219554215519032344086824849470294473808177223497912069335635933312949412445851201918768630656712413082629164792850095444166888072453190903931430551124946191872759 +ANLs7OsR7oBM5jSjVADrk+Mx9d0TeieTIkxwWiJ5STKNQmW2EzPOjgbfcLhbYEhzzDFJveXO2dzz6/c8V5oW2yqg7VMx88DzEbpQnQpk/rOQRw9jbI4fxXNJHkNZCeysEVvFfLJb4ecsGA0xJ3Aylny/jP10ahPv2z5K99edGZSU 148116916208650944522110872759145096907599612943009577897396622287067669897712748449324334650112672914917664881091633448764667172850435775162090891556266912697811031318228334453406561952979778127173704706529448647577013482442758465809198730066784986763500579667100246958959793527011919373534159474250508506260 +AL+Er3n1qj+SBsZVtOMJYg4m0CN+DE6gRnC1F7nPvd2XnBe+QE0+LKfcpUDHVNxoydW4BDzNVwnUNbyjXZ+iuddPtO9hchVEI36UiuL0ydeldFpOZ9mtHJaAF6abd0MlHw4vXRf8CbOvXb5N4s76ggijlZBjRtU563sSmBcyq6Zt 134488725667189507159811764480908602790838430340670328479145818969651133017546803581865897303917708192047926432630297993507146075655594931523561067937580218599890162311074002344315818494246433967228889645359283635389151927472221799543158424012020308449895562192866672439712148770104592027035768027605661099629 +AK/04XOBSjjPpuFXTDF82RNWnKqZz9mJQbS2B5bn0ehFnBa6j+B+MazX+AxXTL/d5+hPLT1uexcnSMl3DcGGwKipOXg7Dtuj3pfJXHTrCqXAUYrIXI+8vKVQO55yQPGfzIg9SVgetwW1sDk+a28ZhJ5a9OddqNoi5C+dLce7ZtNb 123560902006294001923570614486104726169564351074482936927091682096999779538353161007361361829586988452098646362280351148131540524964916445100589671458589346440250329883789099771417949746709217272531950438336245613419967556433467843237384555807236658182067742367748737224684334525934210197178231424396818830171 +PzOEGHlihiveoWFAALY+LOfkRJfm0NUF/uR6cSU/tbpGAq4onNpr+iZIzEP5o3JBLOtDC595/NBPI0fzaXl0vQvgJs6KG8iKANjsLKQjIpZBkoKhdbG9MzTVQuAeuDW0w3sn2iMZ/v2dgAzRwfqmQYXJr3I2BbcwWraIJuZXw5A= 44381416070253681813077725822442106641846565789204187691647505370231831464947935035197059366680327425453811558282831465960889061956588244308214943856009686127871667376028831540813257349779756631357122923723235595360268572998278795110672666089470210929411514949652537714634611421849780859192966935514197771152 +APnuduN01GS9dO2m2uCLs400AR2lX7elOnIPC5U6e17qbukxWYzNhilZlM4kdGXAIeYpzFdSIW/gxRMZe6TXq9krFWRaaPyT2QwRfGHYnazS9F1QNYmW1zXdt+qVp0JGxmh5PyDstbP8Z3x50/E8Mb0gLLPhNAvzY2Jnr9A8Q1Hy 175507868985304663005133968393406051624825489142498103948374797086106732382869120248515993626061853699363294022457032257026588816021007648668265488426495800459085474654859258116280251546902009156490112550154951965894022789029787886785376415437170872937201839249103828294508088966180386198213606090453461193202 +QHEhL4iVzNdUsfG0izTEepwTOvxka8t/9MwuF1Ey6kxsI+ry4g4sJPgR2xMnbtOmvQn2NitAkfvA8JPCiL7a8+gmf+DVRDjKDfpfrtgAVmo+3rH+uJYTrKhAp8R7ggU2xIrvbIrgeUj7ieThPI3Rtap+IdkPCL853JC/+oKtytM= 45252649968839515171157821292772647085425694172492111870169593872127007254353374581972876464918186509502070064028725519394859148593053614163356612260257013360168930649423732336969778875205250872728821432415158634190866775855521719727700464116412886964736859295086745723651735554245035077902615220578218265299 +APeaekK4mVhEShCfM0mkRebcg1Iq5CgrFIEGOoh1nHzgebr5A9Wrhm9yD1Vd3e+fFD9urDRB4y5MHPJHX1U2NFToC+H8nQkFXL8bfd/9Wl2c7y8m0Mxwi53pLIdzETLbbfeOOtJvuSYYT3n8+/PeMnJ46UD8OfqtnFuS0/bVpFLS 173873040145444066957050580959132871919216036714423404143335635770937773583761934638398867981658394368476005882852706046614562314432695052874974848076542261910660410561876043187368112065303981001507235893831108658530338308496461162623683138693880482650786841100027392293758260448606244283355655751440485602002 +FqC/wgZDPTUoObPFSH5w4QR79zj/O+ZiHGTEnsBMwNZD3Gl/ClRDIsFMDDupNLgwgXsqCQbpwSOHOtAvUuAFwRpzt5B7lwIgtP5ism/AZRno5p+9WVSmUAM3glHsNtvYydz2MkXtnXzSMIR1ZVoLrdwMnckE4pbMzggqz+JZqxw= 15889870005716350976759704672045310928616256175405784574141006779373730686049218680335525720670897894546334915362899913262232170795516176419192840427996647372619000239408311568577050460995518058850793096827271653902583271225799114408537346367483775593212272587811309978019791973449354003275559762102731778844 +AJXNbv2AMWadF5h99ZAUy5gLnVK/hMaakFo0ZedtPNRJobxPmwj+h52G+Czd0U48G0V0wpdeUJC9v/4BhjzhCvNhNsdAT1+vQXDuteYQ1aspsEKLQ6b+NknO88QSbRJw53+KeOY2xe7PKOa4V89XnFFBF7wljRnIYrM8vvcqVQDk 105194875227030598769888785590198577650278341586165110611689226597424766274486797264032300493674927704016605741286512271390703088626381669060095573361828932336327125438452066548897528158329044309005232090053420259033538936293519762277428283316506398965916381374819450858053512398634116052299066189424983605476 +AIDRnUpBHepjBqYAlU4MG/8JxzX1mPxVNHpWvnEVgvqTQx/bisFPpXrYs3jAKIR/lzevYwhH0K/8Vvw4NK9iTMFqgSnU44AZztKsoxUXsEsl1UU56UscY5C7ciKU6vjjWI7nm/uHNOXdE82TQXkk2WX8ferNqZU5DaLFCb+zxb7w 90459642084794142567976043425270153270545560059973413835786695756473295513758287577749768786155290305189883600338986370836806413936196854410098516254596146039255388020628703824195128439558127783534033672712705194483515442668075394018677699876614329419492391568463215822656901183478205197671375262145069825776 +AIdvVNzJqWPgAShvi3GhbhMQft+SLigKGrhoqas2Saz/bA9u9Td6fAxa2LjrAqshW6cnm2aalc3Yv6RW/Y8vg7Ho31NSaRjT4zMUenykcC0/Y88UNxREi85wdnHwGytms6Lq49H8/7EFGJIyL1PLRWPmZn6XFkegscI/HUq/hiKm 95105613103051650721863964216778532448106311156426028879315612217763044797186635476805213120469258258125661666950525364331551671653846368977016286153840829836509696804585927581668281228810410814602664419962214359687545209312836366693384158782798559255789953908588601637765910472073600954502095647132310971046 +DdchOPjXrI6lpV84IdKCisPmdqZan8AARXRLADEhixsfXCYuO+WhNatI/fM1vgv+/TxwwIQjIfG1vOZcB36JUfjHYdItYQ70vUXaVFdpqvoBGyfOTU50Ds/11iGPCF8mWiQwR30/XAXytqDZtaVJVWsgHD3RigBSnSHhnvZAWYg= 9719024770319024562623340689338530708271347986326272393419504304391837979619189392867902307307106771234732135400958362219711925045600118964223238147375808749507928768896918369395426933218443166133187066167663170936604731896932630589251946733237697936733924510107175304126061649311812536190882160340308613512 +I+Z6rdTOt26/v3dtUP1plITb15fjb6aMDvqFS3AD1+nxBqnnk7ISGE9j6dv762EIWQpMzcCG5NCCq35KOHEwRXP28zup6olOMt3CBFgYVcBE2pWOpGiO19G/iFweYZXZPY5HgIkex7HBbb7l6HhomPc2sLL/IRhh2oogyHx2JMM= 25210054612455888156900839678249806510561198051210010474517915819801056434402727631042894881559517808906460418029149538469607239850657781476308872923928122553395468026744382526167194202058040459679991391557937527079948356545086684521068912222036707113005006607012596093923970784177288565193670152033981048003 +ALbBoyelCs4UkfnPjMT3S67ujhBHBEE0uxLx6kSGZq2IOMU/QdWYPFElRgYC/y++334FSEycjS6NAJJo2ITpZCO5AjNJ93J3WYgbDLiwu1VzKHX6ItfFNEk45km+QTi07+pDKcKNd1k0mxqpLd/PuZd5hRpPDDoKBb6i+mrCb2yF 128335905497646745013379107761994003743181143126608677203818152878840562628631384684712779135591095534911406031545494164782375276574093777950840330452805743803067864740000758175436633463846967335728314347497013853264454015790847388463800323796888198433722196292529074568758149650782323407298620158495364705413 +ANwlxEkeqmqYTxw1ZwMi1v2wo4ntPaEYZYoTLTJQfa+kuIksnHW9va243HAiOixd+rviVdm1dEwzESBbX0wiJNtRBpP+bnRxy4xOBjNoOB0c/tfka5JVwu5eeskyHx4V3inLviUaj86Yck42n5NaJFMfBvhzVftZ/YF9WBITI8g6 154592850289860621115358362871905683265658659789986179554827712019629689749439795961607030363152337159590319622241556795951071651584979664762468782303706550885785493534656062553770262954861884613383561063525714923031691298088562054236178003658891902606245782350998076658704876516153027797371814038658244397114 diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/oidutil.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/oidutil.py new file mode 100644 index 0000000..fbeadf2 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/oidutil.py @@ -0,0 +1,176 @@ +import unittest +import codecs +import string +import random +from openid import oidutil + +def test_base64(): + allowed_s = string.ascii_letters + string.digits + '+/=' + allowed_d = {} + for c in allowed_s: + allowed_d[c] = None + isAllowed = allowed_d.has_key + + def checkEncoded(s): + for c in s: + assert isAllowed(c), s + + cases = [ + '', + 'x', + '\x00', + '\x01', + '\x00' * 100, + ''.join(map(chr, range(256))), + ] + + for s in cases: + b64 = oidutil.toBase64(s) + checkEncoded(b64) + s_prime = oidutil.fromBase64(b64) + assert s_prime == s, (s, b64, s_prime) + + # Randomized test + for _ in xrange(50): + n = random.randrange(2048) + s = ''.join(map(chr, map(lambda _: random.randrange(256), range(n)))) + b64 = oidutil.toBase64(s) + checkEncoded(b64) + s_prime = oidutil.fromBase64(b64) + assert s_prime == s, (s, b64, s_prime) + +class AppendArgsTest(unittest.TestCase): + def __init__(self, desc, args, expected): + unittest.TestCase.__init__(self) + self.desc = desc + self.args = args + self.expected = expected + + def runTest(self): + result = oidutil.appendArgs(*self.args) + self.assertEqual(self.expected, result, self.args) + + def shortDescription(self): + return self.desc + + + +class TestSymbol(unittest.TestCase): + def testCopyHash(self): + import copy + s = oidutil.Symbol("Foo") + d = {s: 1} + d_prime = copy.deepcopy(d) + self.failUnless(s in d_prime, "%r isn't in %r" % (s, d_prime)) + + t = oidutil.Symbol("Bar") + self.failIfEqual(hash(s), hash(t)) + + +def buildAppendTests(): + simple = 'http://www.example.com/' + cases = [ + ('empty list', + (simple, []), + simple), + + ('empty dict', + (simple, {}), + simple), + + ('one list', + (simple, [('a', 'b')]), + simple + '?a=b'), + + ('one dict', + (simple, {'a':'b'}), + simple + '?a=b'), + + ('two list (same)', + (simple, [('a', 'b'), ('a', 'c')]), + simple + '?a=b&a=c'), + + ('two list', + (simple, [('a', 'b'), ('b', 'c')]), + simple + '?a=b&b=c'), + + ('two list (order)', + (simple, [('b', 'c'), ('a', 'b')]), + simple + '?b=c&a=b'), + + ('two dict (order)', + (simple, {'b':'c', 'a':'b'}), + simple + '?a=b&b=c'), + + ('escape', + (simple, [('=', '=')]), + simple + '?%3D=%3D'), + + ('escape (URL)', + (simple, [('this_url', simple)]), + simple + '?this_url=http%3A%2F%2Fwww.example.com%2F'), + + ('use dots', + (simple, [('openid.stuff', 'bother')]), + simple + '?openid.stuff=bother'), + + ('args exist (empty)', + (simple + '?stuff=bother', []), + simple + '?stuff=bother'), + + ('args exist', + (simple + '?stuff=bother', [('ack', 'ack')]), + simple + '?stuff=bother&ack=ack'), + + ('args exist', + (simple + '?stuff=bother', [('ack', 'ack')]), + simple + '?stuff=bother&ack=ack'), + + ('args exist (dict)', + (simple + '?stuff=bother', {'ack': 'ack'}), + simple + '?stuff=bother&ack=ack'), + + ('args exist (dict 2)', + (simple + '?stuff=bother', {'ack': 'ack', 'zebra':'lion'}), + simple + '?stuff=bother&ack=ack&zebra=lion'), + + ('three args (dict)', + (simple, {'stuff': 'bother', 'ack': 'ack', 'zebra':'lion'}), + simple + '?ack=ack&stuff=bother&zebra=lion'), + + ('three args (list)', + (simple, [('stuff', 'bother'), ('ack', 'ack'), ('zebra', 'lion')]), + simple + '?stuff=bother&ack=ack&zebra=lion'), + ] + + tests = [] + + for name, args, expected in cases: + test = AppendArgsTest(name, args, expected) + tests.append(test) + + return unittest.TestSuite(tests) + +def pyUnitTests(): + some = buildAppendTests() + some.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestSymbol)) + return some + +def test_appendArgs(): + suite = buildAppendTests() + suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(TestSymbol)) + runner = unittest.TextTestRunner() + result = runner.run(suite) + assert result.wasSuccessful() + +# XXX: there are more functions that could benefit from being better +# specified and tested in oidutil.py These include, but are not +# limited to appendArgs + +def test(skipPyUnit=True): + test_base64() + if not skipPyUnit: + test_appendArgs() + +if __name__ == '__main__': + test(skipPyUnit=False) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/storetest.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/storetest.py new file mode 100644 index 0000000..e428c36 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/storetest.py @@ -0,0 +1,397 @@ +from openid.association import Association +from openid.cryptutil import randomString +from openid.store.nonce import mkNonce, split + +import unittest +import string +import time +import socket +import random +import os + +db_host = 'dbtest' + +allowed_handle = [] +for c in string.printable: + if c not in string.whitespace: + allowed_handle.append(c) +allowed_handle = ''.join(allowed_handle) + +def generateHandle(n): + return randomString(n, allowed_handle) + +generateSecret = randomString + +def getTmpDbName(): + hostname = socket.gethostname() + hostname = hostname.replace('.', '_') + hostname = hostname.replace('-', '_') + return "%s_%d_%s_openid_test" % \ + (hostname, os.getpid(), \ + random.randrange(1, int(time.time()))) + +def testStore(store): + """Make sure a given store has a minimum of API compliance. Call + this function with an empty store. + + Raises AssertionError if the store does not work as expected. + + OpenIDStore -> NoneType + """ + ### Association functions + now = int(time.time()) + + server_url = 'http://www.myopenid.com/openid' + def genAssoc(issued, lifetime=600): + sec = generateSecret(20) + hdl = generateHandle(128) + return Association(hdl, sec, now + issued, lifetime, 'HMAC-SHA1') + + def checkRetrieve(url, handle=None, expected=None): + retrieved_assoc = store.getAssociation(url, handle) + assert retrieved_assoc == expected, (retrieved_assoc, expected) + if expected is not None: + if retrieved_assoc is expected: + print ('Unexpected: retrieved a reference to the expected ' + 'value instead of a new object') + assert retrieved_assoc.handle == expected.handle + assert retrieved_assoc.secret == expected.secret + + def checkRemove(url, handle, expected): + present = store.removeAssociation(url, handle) + assert bool(expected) == bool(present) + + assoc = genAssoc(issued=0) + + # Make sure that a missing association returns no result + checkRetrieve(server_url) + + # Check that after storage, getting returns the same result + store.storeAssociation(server_url, assoc) + checkRetrieve(server_url, None, assoc) + + # more than once + checkRetrieve(server_url, None, assoc) + + # Storing more than once has no ill effect + store.storeAssociation(server_url, assoc) + checkRetrieve(server_url, None, assoc) + + # Removing an association that does not exist returns not present + checkRemove(server_url, assoc.handle + 'x', False) + + # Removing an association that does not exist returns not present + checkRemove(server_url + 'x', assoc.handle, False) + + # Removing an association that is present returns present + checkRemove(server_url, assoc.handle, True) + + # but not present on subsequent calls + checkRemove(server_url, assoc.handle, False) + + # Put assoc back in the store + store.storeAssociation(server_url, assoc) + + # More recent and expires after assoc + assoc2 = genAssoc(issued=1) + store.storeAssociation(server_url, assoc2) + + # After storing an association with a different handle, but the + # same server_url, the handle with the later issue date is returned. + checkRetrieve(server_url, None, assoc2) + + # We can still retrieve the older association + checkRetrieve(server_url, assoc.handle, assoc) + + # Plus we can retrieve the association with the later issue date + # explicitly + checkRetrieve(server_url, assoc2.handle, assoc2) + + # More recent, and expires earlier than assoc2 or assoc. Make sure + # that we're picking the one with the latest issued date and not + # taking into account the expiration. + assoc3 = genAssoc(issued=2, lifetime=100) + store.storeAssociation(server_url, assoc3) + + checkRetrieve(server_url, None, assoc3) + checkRetrieve(server_url, assoc.handle, assoc) + checkRetrieve(server_url, assoc2.handle, assoc2) + checkRetrieve(server_url, assoc3.handle, assoc3) + + checkRemove(server_url, assoc2.handle, True) + + checkRetrieve(server_url, None, assoc3) + checkRetrieve(server_url, assoc.handle, assoc) + checkRetrieve(server_url, assoc2.handle, None) + checkRetrieve(server_url, assoc3.handle, assoc3) + + checkRemove(server_url, assoc2.handle, False) + checkRemove(server_url, assoc3.handle, True) + + checkRetrieve(server_url, None, assoc) + checkRetrieve(server_url, assoc.handle, assoc) + checkRetrieve(server_url, assoc2.handle, None) + checkRetrieve(server_url, assoc3.handle, None) + + checkRemove(server_url, assoc2.handle, False) + checkRemove(server_url, assoc.handle, True) + checkRemove(server_url, assoc3.handle, False) + + checkRetrieve(server_url, None, None) + checkRetrieve(server_url, assoc.handle, None) + checkRetrieve(server_url, assoc2.handle, None) + checkRetrieve(server_url, assoc3.handle, None) + + checkRemove(server_url, assoc2.handle, False) + checkRemove(server_url, assoc.handle, False) + checkRemove(server_url, assoc3.handle, False) + + ### test expired associations + # assoc 1: server 1, valid + # assoc 2: server 1, expired + # assoc 3: server 2, expired + # assoc 4: server 3, valid + assocValid1 = genAssoc(issued=-3600,lifetime=7200) + assocValid2 = genAssoc(issued=-5) + assocExpired1 = genAssoc(issued=-7200,lifetime=3600) + assocExpired2 = genAssoc(issued=-7200,lifetime=3600) + + store.cleanupAssociations() + store.storeAssociation(server_url + '1', assocValid1) + store.storeAssociation(server_url + '1', assocExpired1) + store.storeAssociation(server_url + '2', assocExpired2) + store.storeAssociation(server_url + '3', assocValid2) + + cleaned = store.cleanupAssociations() + assert cleaned == 2, cleaned + + ### Nonce functions + + def checkUseNonce(nonce, expected, server_url, msg=''): + stamp, salt = split(nonce) + actual = store.useNonce(server_url, stamp, salt) + assert bool(actual) == bool(expected), "%r != %r: %s" % (actual, expected, + msg) + + for url in [server_url, '']: + # Random nonce (not in store) + nonce1 = mkNonce() + + # A nonce is allowed by default + checkUseNonce(nonce1, True, url) + + # Storing once causes useNonce to return True the first, and only + # the first, time it is called after the store. + checkUseNonce(nonce1, False, url) + checkUseNonce(nonce1, False, url) + + # Nonces from when the universe was an hour old should not pass these days. + old_nonce = mkNonce(3600) + checkUseNonce(old_nonce, False, url, "Old nonce (%r) passed." % (old_nonce,)) + + + old_nonce1 = mkNonce(now - 20000) + old_nonce2 = mkNonce(now - 10000) + recent_nonce = mkNonce(now - 600) + + from openid.store import nonce as nonceModule + orig_skew = nonceModule.SKEW + try: + nonceModule.SKEW = 0 + store.cleanupNonces() + # Set SKEW high so stores will keep our nonces. + nonceModule.SKEW = 100000 + assert store.useNonce(server_url, *split(old_nonce1)) + assert store.useNonce(server_url, *split(old_nonce2)) + assert store.useNonce(server_url, *split(recent_nonce)) + + nonceModule.SKEW = 3600 + cleaned = store.cleanupNonces() + assert cleaned == 2, "Cleaned %r nonces." % (cleaned,) + + nonceModule.SKEW = 100000 + # A roundabout method of checking that the old nonces were cleaned is + # to see if we're allowed to add them again. + assert store.useNonce(server_url, *split(old_nonce1)) + assert store.useNonce(server_url, *split(old_nonce2)) + # The recent nonce wasn't cleaned, so it should still fail. + assert not store.useNonce(server_url, *split(recent_nonce)) + finally: + nonceModule.SKEW = orig_skew + + +def test_filestore(): + from openid.store import filestore + import tempfile + import shutil + try: + temp_dir = tempfile.mkdtemp() + except AttributeError: + import os + temp_dir = os.tmpnam() + os.mkdir(temp_dir) + + store = filestore.FileOpenIDStore(temp_dir) + try: + testStore(store) + store.cleanup() + except: + raise + else: + shutil.rmtree(temp_dir) + +def test_sqlite(): + from openid.store import sqlstore + try: + from pysqlite2 import dbapi2 as sqlite + except ImportError: + pass + else: + conn = sqlite.connect(':memory:') + store = sqlstore.SQLiteStore(conn) + store.createTables() + testStore(store) + +def test_mysql(): + from openid.store import sqlstore + try: + import MySQLdb + except ImportError: + pass + else: + db_user = 'openid_test' + db_passwd = '' + db_name = getTmpDbName() + + from MySQLdb.constants import ER + + # Change this connect line to use the right user and password + try: + conn = MySQLdb.connect(user=db_user, passwd=db_passwd, host = db_host) + except MySQLdb.OperationalError, why: + if why[0] == 2005: + print ('Skipping MySQL store test (cannot connect ' + 'to test server on host %r)' % (db_host,)) + return + else: + raise + + conn.query('CREATE DATABASE %s;' % db_name) + try: + conn.query('USE %s;' % db_name) + + # OK, we're in the right environment. Create store and + # create the tables. + store = sqlstore.MySQLStore(conn) + store.createTables() + + # At last, we get to run the test. + testStore(store) + finally: + # Remove the database. If you want to do post-mortem on a + # failing test, comment out this line. + conn.query('DROP DATABASE %s;' % db_name) + +def test_postgresql(): + """ + Tests the PostgreSQLStore on a locally-hosted PostgreSQL database + cluster, version 7.4 or later. To run this test, you must have: + + - The 'psycopg' python module (version 1.1) installed + + - PostgreSQL running locally + + - An 'openid_test' user account in your database cluster, which + you can create by running 'createuser -Ad openid_test' as the + 'postgres' user + + - Trust auth for the 'openid_test' account, which you can activate + by adding the following line to your pg_hba.conf file: + + local all openid_test trust + + This test connects to the database cluster three times: + + - To the 'template1' database, to create the test database + + - To the test database, to run the store tests + + - To the 'template1' database once more, to drop the test database + """ + from openid.store import sqlstore + try: + import psycopg + except ImportError: + pass + else: + db_name = getTmpDbName() + db_user = 'openid_test' + + # Connect once to create the database; reconnect to access the + # new database. + conn_create = psycopg.connect(database = 'template1', user = db_user, + host = db_host) + conn_create.autocommit() + + # Create the test database. + cursor = conn_create.cursor() + cursor.execute('CREATE DATABASE %s;' % (db_name,)) + conn_create.close() + + # Connect to the test database. + conn_test = psycopg.connect(database = db_name, user = db_user, + host = db_host) + + # OK, we're in the right environment. Create the store + # instance and create the tables. + store = sqlstore.PostgreSQLStore(conn_test) + store.createTables() + + # At last, we get to run the test. + testStore(store) + + # Disconnect. + conn_test.close() + + # It takes a little time for the close() call above to take + # effect, so we'll wait for a second before trying to remove + # the database. (Maybe this is because we're using a UNIX + # socket to connect to postgres rather than TCP?) + import time + time.sleep(1) + + # Remove the database now that the test is over. + conn_remove = psycopg.connect(database = 'template1', user = db_user, + host = db_host) + conn_remove.autocommit() + + cursor = conn_remove.cursor() + cursor.execute('DROP DATABASE %s;' % (db_name,)) + conn_remove.close() + +def test_memstore(): + from openid.store import memstore + testStore(memstore.MemoryStore()) + +test_functions = [ + test_filestore, + test_sqlite, + test_mysql, + test_postgresql, + test_memstore, + ] + +def pyUnitTests(): + tests = map(unittest.FunctionTestCase, test_functions) + load = unittest.defaultTestLoader.loadTestsFromTestCase + return unittest.TestSuite(tests) + +if __name__ == '__main__': + import sys + suite = pyUnitTests() + runner = unittest.TextTestRunner() + result = runner.run(suite) + if result.wasSuccessful(): + sys.exit(0) + else: + sys.exit(1) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/support.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/support.py new file mode 100644 index 0000000..dbf8881 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/support.py @@ -0,0 +1,51 @@ +from openid import message +from openid import oidutil + +class OpenIDTestMixin(object): + def failUnlessOpenIDValueEquals(self, msg, key, expected, ns=None): + if ns is None: + ns = message.OPENID_NS + + actual = msg.getArg(ns, key) + error_format = 'Wrong value for openid.%s: expected=%s, actual=%s' + error_message = error_format % (key, expected, actual) + self.failUnlessEqual(expected, actual, error_message) + + def failIfOpenIDKeyExists(self, msg, key, ns=None): + if ns is None: + ns = message.OPENID_NS + + actual = msg.getArg(ns, key) + error_message = 'openid.%s unexpectedly present: %s' % (key, actual) + self.failIf(actual is not None, error_message) + +class CatchLogs(object): + def setUp(self): + self.old_logger = oidutil.log + oidutil.log = self.gotLogMessage + self.messages = [] + + def gotLogMessage(self, message): + self.messages.append(message) + + def tearDown(self): + oidutil.log = self.old_logger + + def failUnlessLogMatches(self, *prefixes): + """ + Check that the log messages contained in self.messages have + prefixes in *prefixes. Raise AssertionError if not, or if the + number of prefixes is different than the number of log + messages. + """ + assert len(prefixes) == len(self.messages), \ + "Expected log prefixes %r, got %r" % (prefixes, + self.messages) + + for prefix, message in zip(prefixes, self.messages): + assert message.startswith(prefix), \ + "Expected log prefixes %r, got %r" % (prefixes, + self.messages) + + def failUnlessLogEmpty(self): + self.failUnlessLogMatches() diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_accept.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_accept.py new file mode 100644 index 0000000..3cc3602 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_accept.py @@ -0,0 +1,127 @@ +import unittest +import os.path +from openid.yadis import accept + +def getTestData(): + """Read the test data off of disk + + () -> [(int, str)] + """ + filename = os.path.join(os.path.dirname(__file__), 'data', 'accept.txt') + i = 1 + lines = [] + for line in file(filename): + lines.append((i, line)) + i += 1 + return lines + +def chunk(lines): + """Return groups of lines separated by whitespace or comments + + [(int, str)] -> [[(int, str)]] + """ + chunks = [] + chunk = [] + for lineno, line in lines: + stripped = line.strip() + if not stripped or stripped[0] == '#': + if chunk: + chunks.append(chunk) + chunk = [] + else: + chunk.append((lineno, stripped)) + + if chunk: + chunks.append(chunk) + + return chunks + +def parseLines(chunk): + """Take the given chunk of lines and turn it into a test data dictionary + + [(int, str)] -> {str:(int, str)} + """ + items = {} + for (lineno, line) in chunk: + header, data = line.split(':', 1) + header = header.lower() + items[header] = (lineno, data.strip()) + + return items + +def parseAvailable(available_text): + """Parse an Available: line's data + + str -> [str] + """ + return [s.strip() for s in available_text.split(',')] + +def parseExpected(expected_text): + """Parse an Expected: line's data + + str -> [(str, float)] + """ + expected = [] + if expected_text: + for chunk in expected_text.split(','): + chunk = chunk.strip() + mtype, qstuff = chunk.split(';') + mtype = mtype.strip() + assert '/' in mtype + qstuff = qstuff.strip() + q, qstr = qstuff.split('=') + assert q == 'q' + qval = float(qstr) + expected.append((mtype, qval)) + + return expected + +class MatchAcceptTest(unittest.TestCase): + def __init__(self, descr, accept_header, available, expected): + unittest.TestCase.__init__(self) + self.accept_header = accept_header + self.available = available + self.expected = expected + self.descr = descr + + def shortDescription(self): + return self.descr + + def runTest(self): + accepted = accept.parseAcceptHeader(self.accept_header) + actual = accept.matchTypes(accepted, self.available) + self.failUnlessEqual(self.expected, actual) + +def pyUnitTests(): + lines = getTestData() + chunks = chunk(lines) + data_sets = map(parseLines, chunks) + cases = [] + for data in data_sets: + lnos = [] + lno, header = data['accept'] + lnos.append(lno) + lno, avail_data = data['available'] + lnos.append(lno) + try: + available = parseAvailable(avail_data) + except: + print 'On line', lno + raise + + lno, exp_data = data['expected'] + lnos.append(lno) + try: + expected = parseExpected(exp_data) + except: + print 'On line', lno + raise + + descr = 'MatchAcceptTest for lines %r' % (lnos,) + case = MatchAcceptTest(descr, header, available, expected) + cases.append(case) + return unittest.TestSuite(cases) + +if __name__ == '__main__': + runner = unittest.TextTestRunner() + runner.run(loadTests()) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_association.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_association.py new file mode 100644 index 0000000..6404a00 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_association.py @@ -0,0 +1,183 @@ +from openid.test import datadriven + +import unittest + +from openid.message import Message, BARE_NS, OPENID_NS, OPENID2_NS +from openid import association +import time +from openid import cryptutil +import warnings + +class AssociationSerializationTest(unittest.TestCase): + def test_roundTrip(self): + issued = int(time.time()) + lifetime = 600 + assoc = association.Association( + 'handle', 'secret', issued, lifetime, 'HMAC-SHA1') + s = assoc.serialize() + assoc2 = association.Association.deserialize(s) + self.failUnlessEqual(assoc.handle, assoc2.handle) + self.failUnlessEqual(assoc.issued, assoc2.issued) + self.failUnlessEqual(assoc.secret, assoc2.secret) + self.failUnlessEqual(assoc.lifetime, assoc2.lifetime) + self.failUnlessEqual(assoc.assoc_type, assoc2.assoc_type) + +from openid.server.server import \ + DiffieHellmanSHA1ServerSession, \ + DiffieHellmanSHA256ServerSession, \ + PlainTextServerSession + +from openid.consumer.consumer import \ + DiffieHellmanSHA1ConsumerSession, \ + DiffieHellmanSHA256ConsumerSession, \ + PlainTextConsumerSession + +from openid.dh import DiffieHellman + +def createNonstandardConsumerDH(): + nonstandard_dh = DiffieHellman(1315291, 2) + return DiffieHellmanSHA1ConsumerSession(nonstandard_dh) + +class DiffieHellmanSessionTest(datadriven.DataDrivenTestCase): + secrets = [ + '\x00' * 20, + '\xff' * 20, + ' ' * 20, + 'This is a secret....', + ] + + session_factories = [ + (DiffieHellmanSHA1ConsumerSession, DiffieHellmanSHA1ServerSession), + (createNonstandardConsumerDH, DiffieHellmanSHA1ServerSession), + (PlainTextConsumerSession, PlainTextServerSession), + ] + + def generateCases(cls): + return [(c, s, sec) + for c, s in cls.session_factories + for sec in cls.secrets] + + generateCases = classmethod(generateCases) + + def __init__(self, csess_fact, ssess_fact, secret): + datadriven.DataDrivenTestCase.__init__(self, csess_fact.__name__) + self.secret = secret + self.csess_fact = csess_fact + self.ssess_fact = ssess_fact + + def runOneTest(self): + csess = self.csess_fact() + msg = Message.fromOpenIDArgs(csess.getRequest()) + ssess = self.ssess_fact.fromMessage(msg) + check_secret = csess.extractSecret( + Message.fromOpenIDArgs(ssess.answer(self.secret))) + self.failUnlessEqual(self.secret, check_secret) + + + +class TestMakePairs(unittest.TestCase): + """Check the key-value formatting methods of associations. + """ + + def setUp(self): + self.message = m = Message(OPENID2_NS) + m.updateArgs(OPENID2_NS, { + 'mode': 'id_res', + 'identifier': '=example', + 'signed': 'identifier,mode', + 'sig': 'cephalopod', + }) + m.updateArgs(BARE_NS, {'xey': 'value'}) + self.assoc = association.Association.fromExpiresIn( + 3600, '{sha1}', 'very_secret', "HMAC-SHA1") + + + def testMakePairs(self): + """Make pairs using the OpenID 1.x type signed list.""" + pairs = self.assoc._makePairs(self.message) + expected = [ + ('identifier', '=example'), + ('mode', 'id_res'), + ] + self.failUnlessEqual(pairs, expected) + + + +class TestMac(unittest.TestCase): + def setUp(self): + self.pairs = [('key1', 'value1'), + ('key2', 'value2')] + + + def test_sha1(self): + assoc = association.Association.fromExpiresIn( + 3600, '{sha1}', 'very_secret', "HMAC-SHA1") + expected = ('\xe0\x1bv\x04\xf1G\xc0\xbb\x7f\x9a\x8b' + '\xe9\xbc\xee}\\\xe5\xbb7*') + sig = assoc.sign(self.pairs) + self.failUnlessEqual(sig, expected) + + if cryptutil.SHA256_AVAILABLE: + def test_sha256(self): + assoc = association.Association.fromExpiresIn( + 3600, '{sha256SA}', 'very_secret', "HMAC-SHA256") + expected = ('\xfd\xaa\xfe;\xac\xfc*\x988\xad\x05d6-\xeaVy' + '\xd5\xa5Z.<\xa9\xed\x18\x82\\$\x95x\x1c&') + sig = assoc.sign(self.pairs) + self.failUnlessEqual(sig, expected) + + + +class TestMessageSigning(unittest.TestCase): + def setUp(self): + self.message = m = Message(OPENID2_NS) + m.updateArgs(OPENID2_NS, {'mode': 'id_res', + 'identifier': '=example'}) + m.updateArgs(BARE_NS, {'xey': 'value'}) + self.args = {'openid.mode': 'id_res', + 'openid.identifier': '=example', + 'xey': 'value'} + + + def test_signSHA1(self): + assoc = association.Association.fromExpiresIn( + 3600, '{sha1}', 'very_secret', "HMAC-SHA1") + signed = assoc.signMessage(self.message) + self.failUnless(signed.getArg(OPENID_NS, "sig")) + self.failUnlessEqual(signed.getArg(OPENID_NS, "signed"), + "assoc_handle,identifier,mode,ns,signed") + self.failUnlessEqual(signed.getArg(BARE_NS, "xey"), "value", + signed) + + if cryptutil.SHA256_AVAILABLE: + def test_signSHA256(self): + assoc = association.Association.fromExpiresIn( + 3600, '{sha1}', 'very_secret', "HMAC-SHA256") + signed = assoc.signMessage(self.message) + self.failUnless(signed.getArg(OPENID_NS, "sig")) + self.failUnlessEqual(signed.getArg(OPENID_NS, "signed"), + "assoc_handle,identifier,mode,ns,signed") + self.failUnlessEqual(signed.getArg(BARE_NS, "xey"), "value", + signed) + + +class TestCheckMessageSignature(unittest.TestCase): + def test_aintGotSignedList(self): + m = Message(OPENID2_NS) + m.updateArgs(OPENID2_NS, {'mode': 'id_res', + 'identifier': '=example', + 'sig': 'coyote', + }) + m.updateArgs(BARE_NS, {'xey': 'value'}) + assoc = association.Association.fromExpiresIn( + 3600, '{sha1}', 'very_secret', "HMAC-SHA1") + self.failUnlessRaises(ValueError, assoc.checkMessageSignature, m) + + +def pyUnitTests(): + return datadriven.loadTests(__name__) + +if __name__ == '__main__': + suite = pyUnitTests() + runner = unittest.TextTestRunner() + runner.run(suite) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_association_response.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_association_response.py new file mode 100644 index 0000000..cf9d014 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_association_response.py @@ -0,0 +1,340 @@ +"""Tests for consumer handling of association responses + +This duplicates some things that are covered by test_consumer, but +this works for now. +""" +from openid import oidutil +from openid.test.test_consumer import CatchLogs +from openid.message import Message, OPENID2_NS, OPENID_NS, no_default +from openid.server.server import DiffieHellmanSHA1ServerSession +from openid.consumer.consumer import GenericConsumer, \ + DiffieHellmanSHA1ConsumerSession, ProtocolError +from openid.consumer.discover import OpenIDServiceEndpoint, OPENID_1_1_TYPE, OPENID_2_0_TYPE +from openid.store import memstore +import unittest + +# Some values we can use for convenience (see mkAssocResponse) +association_response_values = { + 'expires_in': '1000', + 'assoc_handle':'a handle', + 'assoc_type':'a type', + 'session_type':'a session type', + 'ns':OPENID2_NS, + } + +def mkAssocResponse(*keys): + """Build an association response message that contains the + specified subset of keys. The values come from + `association_response_values`. + + This is useful for testing for missing keys and other times that + we don't care what the values are.""" + args = dict([(key, association_response_values[key]) for key in keys]) + return Message.fromOpenIDArgs(args) + +class BaseAssocTest(CatchLogs, unittest.TestCase): + def setUp(self): + CatchLogs.setUp(self) + self.store = memstore.MemoryStore() + self.consumer = GenericConsumer(self.store) + self.endpoint = OpenIDServiceEndpoint() + + def failUnlessProtocolError(self, str_prefix, func, *args, **kwargs): + try: + result = func(*args, **kwargs) + except ProtocolError, e: + message = 'Expected prefix %r, got %r' % (str_prefix, e[0]) + self.failUnless(e[0].startswith(str_prefix), message) + else: + self.fail('Expected ProtocolError, got %r' % (result,)) + +def mkExtractAssocMissingTest(keys): + """Factory function for creating test methods for generating + missing field tests. + + Make a test that ensures that an association response that + is missing required fields will short-circuit return None. + + According to 'Association Session Response' subsection 'Common + Response Parameters', the following fields are required for OpenID + 2.0: + + * ns + * session_type + * assoc_handle + * assoc_type + * expires_in + + If 'ns' is missing, it will fall back to OpenID 1 checking. In + OpenID 1, everything except 'session_type' and 'ns' are required. + """ + + def test(self): + msg = mkAssocResponse(*keys) + + self.failUnlessRaises(KeyError, + self.consumer._extractAssociation, msg, None) + + return test + +class TestExtractAssociationMissingFieldsOpenID2(BaseAssocTest): + """Test for returning an error upon missing fields in association + responses for OpenID 2""" + + test_noFields_openid2 = mkExtractAssocMissingTest(['ns']) + + test_missingExpires_openid2 = mkExtractAssocMissingTest( + ['assoc_handle', 'assoc_type', 'session_type', 'ns']) + + test_missingHandle_openid2 = mkExtractAssocMissingTest( + ['expires_in', 'assoc_type', 'session_type', 'ns']) + + test_missingAssocType_openid2 = mkExtractAssocMissingTest( + ['expires_in', 'assoc_handle', 'session_type', 'ns']) + + test_missingSessionType_openid2 = mkExtractAssocMissingTest( + ['expires_in', 'assoc_handle', 'assoc_type', 'ns']) + +class TestExtractAssociationMissingFieldsOpenID1(BaseAssocTest): + """Test for returning an error upon missing fields in association + responses for OpenID 2""" + + test_noFields_openid1 = mkExtractAssocMissingTest([]) + + test_missingExpires_openid1 = mkExtractAssocMissingTest( + ['assoc_handle', 'assoc_type']) + + test_missingHandle_openid1 = mkExtractAssocMissingTest( + ['expires_in', 'assoc_type']) + + test_missingAssocType_openid1 = mkExtractAssocMissingTest( + ['expires_in', 'assoc_handle']) + +class DummyAssocationSession(object): + def __init__(self, session_type, allowed_assoc_types=()): + self.session_type = session_type + self.allowed_assoc_types = allowed_assoc_types + +class ExtractAssociationSessionTypeMismatch(BaseAssocTest): + def mkTest(requested_session_type, response_session_type, openid1=False): + def test(self): + assoc_session = DummyAssocationSession(requested_session_type) + keys = association_response_values.keys() + if openid1: + keys.remove('ns') + msg = mkAssocResponse(*keys) + msg.setArg(OPENID_NS, 'session_type', response_session_type) + self.failUnlessProtocolError('Session type mismatch', + self.consumer._extractAssociation, msg, assoc_session) + + return test + + test_typeMismatchNoEncBlank_openid2 = mkTest( + requested_session_type='no-encryption', + response_session_type='', + ) + + test_typeMismatchDHSHA1NoEnc_openid2 = mkTest( + requested_session_type='DH-SHA1', + response_session_type='no-encryption', + ) + + test_typeMismatchDHSHA256NoEnc_openid2 = mkTest( + requested_session_type='DH-SHA256', + response_session_type='no-encryption', + ) + + test_typeMismatchNoEncDHSHA1_openid2 = mkTest( + requested_session_type='no-encryption', + response_session_type='DH-SHA1', + ) + + test_typeMismatchDHSHA1NoEnc_openid1 = mkTest( + requested_session_type='DH-SHA1', + response_session_type='DH-SHA256', + openid1=True, + ) + + test_typeMismatchDHSHA256NoEnc_openid1 = mkTest( + requested_session_type='DH-SHA256', + response_session_type='DH-SHA1', + openid1=True, + ) + + test_typeMismatchNoEncDHSHA1_openid1 = mkTest( + requested_session_type='no-encryption', + response_session_type='DH-SHA1', + openid1=True, + ) + + +class TestOpenID1AssociationResponseSessionType(BaseAssocTest): + def mkTest(expected_session_type, session_type_value): + """Return a test method that will check what session type will + be used if the OpenID 1 response to an associate call sets the + 'session_type' field to `session_type_value` + """ + def test(self): + self._doTest(expected_session_type, session_type_value) + self.failUnlessEqual(0, len(self.messages)) + + return test + + def _doTest(self, expected_session_type, session_type_value): + # Create a Message with just 'session_type' in it, since + # that's all this function will use. 'session_type' may be + # absent if it's set to None. + args = {} + if session_type_value is not None: + args['session_type'] = session_type_value + message = Message.fromOpenIDArgs(args) + self.failUnless(message.isOpenID1()) + + actual_session_type = self.consumer._getOpenID1SessionType(message) + error_message = ('Returned sesion type parameter %r was expected ' + 'to yield session type %r, but yielded %r' % + (session_type_value, expected_session_type, + actual_session_type)) + self.failUnlessEqual( + expected_session_type, actual_session_type, error_message) + + test_none = mkTest( + session_type_value=None, + expected_session_type='no-encryption', + ) + + test_empty = mkTest( + session_type_value='', + expected_session_type='no-encryption', + ) + + # This one's different because it expects log messages + def test_explicitNoEncryption(self): + self._doTest( + session_type_value='no-encryption', + expected_session_type='no-encryption', + ) + self.failUnlessEqual(1, len(self.messages)) + self.failUnless(self.messages[0].startswith( + 'WARNING: OpenID server sent "no-encryption"')) + + test_dhSHA1 = mkTest( + session_type_value='DH-SHA1', + expected_session_type='DH-SHA1', + ) + + # DH-SHA256 is not a valid session type for OpenID1, but this + # function does not test that. This is mostly just to make sure + # that it will pass-through stuff that is not explicitly handled, + # so it will get handled the same way as it is handled for OpenID + # 2 + test_dhSHA256 = mkTest( + session_type_value='DH-SHA256', + expected_session_type='DH-SHA256', + ) + +class DummyAssociationSession(object): + secret = "shh! don't tell!" + extract_secret_called = False + + session_type = None + + allowed_assoc_types = None + + def extractSecret(self, message): + self.extract_secret_called = True + return self.secret + +class TestInvalidFields(BaseAssocTest): + def setUp(self): + BaseAssocTest.setUp(self) + self.session_type = 'testing-session' + + # This must something that works for Association.fromExpiresIn + self.assoc_type = 'HMAC-SHA1' + + self.assoc_handle = 'testing-assoc-handle' + + # These arguments should all be valid + self.assoc_response = Message.fromOpenIDArgs({ + 'expires_in': '1000', + 'assoc_handle':self.assoc_handle, + 'assoc_type':self.assoc_type, + 'session_type':self.session_type, + 'ns':OPENID2_NS, + }) + + self.assoc_session = DummyAssociationSession() + + # Make the session for the response's session type + self.assoc_session.session_type = self.session_type + self.assoc_session.allowed_assoc_types = [self.assoc_type] + + def test_worksWithGoodFields(self): + """Handle a full successful association response""" + assoc = self.consumer._extractAssociation( + self.assoc_response, self.assoc_session) + self.failUnless(self.assoc_session.extract_secret_called) + self.failUnlessEqual(self.assoc_session.secret, assoc.secret) + self.failUnlessEqual(1000, assoc.lifetime) + self.failUnlessEqual(self.assoc_handle, assoc.handle) + self.failUnlessEqual(self.assoc_type, assoc.assoc_type) + + def test_badAssocType(self): + # Make sure that the assoc type in the response is not valid + # for the given session. + self.assoc_session.allowed_assoc_types = [] + self.failUnlessProtocolError('Unsupported assoc_type for session', + self.consumer._extractAssociation, + self.assoc_response, self.assoc_session) + + def test_badExpiresIn(self): + # Invalid value for expires_in should cause failure + self.assoc_response.setArg(OPENID_NS, 'expires_in', 'forever') + self.failUnlessProtocolError('Invalid expires_in', + self.consumer._extractAssociation, + self.assoc_response, self.assoc_session) + + +# XXX: This is what causes most of the imports in this file. It is +# sort of a unit test and sort of a functional test. I'm not terribly +# fond of it. +class TestExtractAssociationDiffieHellman(BaseAssocTest): + secret = 'x' * 20 + + def _setUpDH(self): + sess, message = self.consumer._createAssociateRequest( + self.endpoint, 'HMAC-SHA1', 'DH-SHA1') + + # XXX: this is testing _createAssociateRequest + self.failUnlessEqual(self.endpoint.compatibilityMode(), + message.isOpenID1()) + + server_sess = DiffieHellmanSHA1ServerSession.fromMessage(message) + server_resp = server_sess.answer(self.secret) + server_resp['assoc_type'] = 'HMAC-SHA1' + server_resp['assoc_handle'] = 'handle' + server_resp['expires_in'] = '1000' + server_resp['session_type'] = 'DH-SHA1' + return sess, Message.fromOpenIDArgs(server_resp) + + def test_success(self): + sess, server_resp = self._setUpDH() + ret = self.consumer._extractAssociation(server_resp, sess) + self.failIf(ret is None) + self.failUnlessEqual(ret.assoc_type, 'HMAC-SHA1') + self.failUnlessEqual(ret.secret, self.secret) + self.failUnlessEqual(ret.handle, 'handle') + self.failUnlessEqual(ret.lifetime, 1000) + + def test_openid2success(self): + # Use openid 2 type in endpoint so _setUpDH checks + # compatibility mode state properly + self.endpoint.type_uris = [OPENID_2_0_TYPE, OPENID_1_1_TYPE] + self.test_success() + + def test_badDHValues(self): + sess, server_resp = self._setUpDH() + server_resp.setArg(OPENID_NS, 'enc_mac_key', '\x00\x00\x00') + self.failUnlessProtocolError('Malformed response for', + self.consumer._extractAssociation, server_resp, sess) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_auth_request.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_auth_request.py new file mode 100644 index 0000000..d9e7233 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_auth_request.py @@ -0,0 +1,206 @@ +import cgi +import unittest + +from openid.consumer import consumer +from openid import message +from openid.test import support + +class DummyEndpoint(object): + preferred_namespace = None + local_id = None + server_url = None + is_op_identifier = False + + def preferredNamespace(self): + return self.preferred_namespace + + def getLocalID(self): + return self.local_id + + def isOPIdentifier(self): + return self.is_op_identifier + +class DummyAssoc(object): + handle = "assoc-handle" + +class TestAuthRequestMixin(support.OpenIDTestMixin): + """Mixin for AuthRequest tests for OpenID 1 and 2; DON'T add + unittest.TestCase as a base class here.""" + + preferred_namespace = None + immediate = False + expected_mode = 'checkid_setup' + + def setUp(self): + self.endpoint = DummyEndpoint() + self.endpoint.local_id = 'http://server.unittest/joe' + self.endpoint.claimed_id = 'http://joe.vanity.example/' + self.endpoint.server_url = 'http://server.unittest/' + self.endpoint.preferred_namespace = self.preferred_namespace + self.realm = 'http://example/' + self.return_to = 'http://example/return/' + self.assoc = DummyAssoc() + self.authreq = consumer.AuthRequest(self.endpoint, self.assoc) + + def failUnlessAnonymous(self, msg): + for key in ['claimed_id', 'identity']: + self.failIfOpenIDKeyExists(msg, key) + + def failUnlessHasRequiredFields(self, msg): + self.failUnlessEqual(self.preferred_namespace, + self.authreq.message.getOpenIDNamespace()) + + self.failUnlessEqual(self.preferred_namespace, + msg.getOpenIDNamespace()) + + self.failUnlessOpenIDValueEquals(msg, 'mode', + self.expected_mode) + + # Implement these in subclasses because they depend on + # protocol differences! + self.failUnlessHasRealm(msg) + self.failUnlessIdentifiersPresent(msg) + + # TESTS + + def test_checkNoAssocHandle(self): + self.authreq.assoc = None + msg = self.authreq.getMessage(self.realm, self.return_to, + self.immediate) + + self.failIfOpenIDKeyExists(msg, 'assoc_handle') + + def test_checkWithAssocHandle(self): + msg = self.authreq.getMessage(self.realm, self.return_to, + self.immediate) + + self.failUnlessOpenIDValueEquals(msg, 'assoc_handle', + self.assoc.handle) + + def test_addExtensionArg(self): + self.authreq.addExtensionArg('bag:', 'color', 'brown') + self.authreq.addExtensionArg('bag:', 'material', 'paper') + self.failUnless('bag:' in self.authreq.message.namespaces) + self.failUnlessEqual(self.authreq.message.getArgs('bag:'), + {'color': 'brown', + 'material': 'paper'}) + msg = self.authreq.getMessage(self.realm, self.return_to, + self.immediate) + + # XXX: this depends on the way that Message assigns + # namespaces. Really it doesn't care that it has alias "0", + # but that is tested anyway + post_args = msg.toPostArgs() + self.failUnlessEqual('brown', post_args['openid.ext0.color']) + self.failUnlessEqual('paper', post_args['openid.ext0.material']) + + def test_standard(self): + msg = self.authreq.getMessage(self.realm, self.return_to, + self.immediate) + + self.failUnlessHasIdentifiers( + msg, self.endpoint.local_id, self.endpoint.claimed_id) + +class TestAuthRequestOpenID2(TestAuthRequestMixin, unittest.TestCase): + preferred_namespace = message.OPENID2_NS + + def failUnlessHasRealm(self, msg): + # check presence of proper realm key and absence of the wrong + # one. + self.failUnlessOpenIDValueEquals(msg, 'realm', self.realm) + self.failIfOpenIDKeyExists(msg, 'trust_root') + + def failUnlessIdentifiersPresent(self, msg): + identity_present = msg.hasKey(message.OPENID_NS, 'identity') + claimed_present = msg.hasKey(message.OPENID_NS, 'claimed_id') + + self.failUnlessEqual(claimed_present, identity_present) + + def failUnlessHasIdentifiers(self, msg, op_specific_id, claimed_id): + self.failUnlessOpenIDValueEquals(msg, 'identity', op_specific_id) + self.failUnlessOpenIDValueEquals(msg, 'claimed_id', claimed_id) + + # TESTS + + def test_setAnonymousWorksForOpenID2(self): + """OpenID AuthRequests should be able to set 'anonymous' to true.""" + self.failUnless(self.authreq.message.isOpenID2()) + self.authreq.setAnonymous(True) + self.authreq.setAnonymous(False) + + def test_userAnonymousIgnoresIdentfier(self): + self.authreq.setAnonymous(True) + msg = self.authreq.getMessage(self.realm, self.return_to, + self.immediate) + self.failUnlessHasRequiredFields(msg) + self.failUnlessAnonymous(msg) + + def test_opAnonymousIgnoresIdentifier(self): + self.endpoint.is_op_identifier = True + self.authreq.setAnonymous(True) + msg = self.authreq.getMessage(self.realm, self.return_to, + self.immediate) + self.failUnlessHasRequiredFields(msg) + self.failUnlessAnonymous(msg) + + def test_opIdentifierSendsIdentifierSelect(self): + self.endpoint.is_op_identifier = True + msg = self.authreq.getMessage(self.realm, self.return_to, + self.immediate) + self.failUnlessHasRequiredFields(msg) + self.failUnlessHasIdentifiers( + msg, message.IDENTIFIER_SELECT, message.IDENTIFIER_SELECT) + +class TestAuthRequestOpenID1(TestAuthRequestMixin, unittest.TestCase): + preferred_namespace = message.OPENID1_NS + + def setUpEndpoint(self): + TestAuthRequestBase.setUpEndpoint(self) + self.endpoint.preferred_namespace = message.OPENID1_NS + + def failUnlessHasIdentifiers(self, msg, op_specific_id, claimed_id): + """Make sure claimed_is is *absent* in request.""" + self.failUnlessOpenIDValueEquals(msg, 'identity', op_specific_id) + self.failIfOpenIDKeyExists(msg, 'claimed_id') + + def failUnlessIdentifiersPresent(self, msg): + self.failIfOpenIDKeyExists(msg, 'claimed_id') + self.failUnless(msg.hasKey(message.OPENID_NS, 'identity')) + + def failUnlessHasRealm(self, msg): + # check presence of proper realm key and absence of the wrong + # one. + self.failUnlessOpenIDValueEquals(msg, 'trust_root', self.realm) + self.failIfOpenIDKeyExists(msg, 'realm') + + # TESTS + + def test_setAnonymousFailsForOpenID1(self): + """OpenID 1 requests MUST NOT be able to set anonymous to True""" + self.failUnless(self.authreq.message.isOpenID1()) + self.failUnlessRaises(ValueError, self.authreq.setAnonymous, True) + self.authreq.setAnonymous(False) + + def test_identifierSelect(self): + """Identfier select SHOULD NOT be sent, but this pathway is in + here in case some special discovery stuff is done to trigger + it with OpenID 1. If it is triggered, it will send + identifier_select just like OpenID 2. + """ + self.endpoint.is_op_identifier = True + msg = self.authreq.getMessage(self.realm, self.return_to, + self.immediate) + self.failUnlessHasRequiredFields(msg) + self.failUnlessEqual(message.IDENTIFIER_SELECT, + msg.getArg(message.OPENID1_NS, 'identity')) + +class TestAuthRequestOpenID1Immediate(TestAuthRequestOpenID1): + immediate = True + expected_mode = 'checkid_immediate' + +class TestAuthRequestOpenID2Immediate(TestAuthRequestOpenID2): + immediate = True + expected_mode = 'checkid_immediate' + +if __name__ == '__main__': + unittest.main() diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_ax.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_ax.py new file mode 100644 index 0000000..9c349a7 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_ax.py @@ -0,0 +1,626 @@ +"""Tests for the attribute exchange extension module +""" + +import unittest +from openid.extensions import ax +from openid.message import NamespaceMap, Message, OPENID2_NS +from openid.consumer.consumer import SuccessResponse + +class BogusAXMessage(ax.AXMessage): + mode = 'bogus' + + getExtensionArgs = ax.AXMessage._newArgs + +class DummyRequest(object): + def __init__(self, message): + self.message = message + +class AXMessageTest(unittest.TestCase): + def setUp(self): + self.bax = BogusAXMessage() + + def test_checkMode(self): + check = self.bax._checkMode + self.failUnlessRaises(ax.NotAXMessage, check, {}) + self.failUnlessRaises(ax.AXError, check, {'mode':'fetch_request'}) + + # does not raise an exception when the mode is right + check({'mode':self.bax.mode}) + + def test_checkMode_newArgs(self): + """_newArgs generates something that has the correct mode""" + # This would raise AXError if it didn't like the mode newArgs made. + self.bax._checkMode(self.bax._newArgs()) + + +class AttrInfoTest(unittest.TestCase): + def test_construct(self): + self.failUnlessRaises(TypeError, ax.AttrInfo) + type_uri = 'a uri' + ainfo = ax.AttrInfo(type_uri) + + self.failUnlessEqual(type_uri, ainfo.type_uri) + self.failUnlessEqual(1, ainfo.count) + self.failIf(ainfo.required) + self.failUnless(ainfo.alias is None) + + +class ToTypeURIsTest(unittest.TestCase): + def setUp(self): + self.aliases = NamespaceMap() + + def test_empty(self): + for empty in [None, '']: + uris = ax.toTypeURIs(self.aliases, empty) + self.failUnlessEqual([], uris) + + def test_undefined(self): + self.failUnlessRaises( + KeyError, + ax.toTypeURIs, self.aliases, 'http://janrain.com/') + + def test_one(self): + uri = 'http://janrain.com/' + alias = 'openid_hackers' + self.aliases.addAlias(uri, alias) + uris = ax.toTypeURIs(self.aliases, alias) + self.failUnlessEqual([uri], uris) + + def test_two(self): + uri1 = 'http://janrain.com/' + alias1 = 'openid_hackers' + self.aliases.addAlias(uri1, alias1) + + uri2 = 'http://jyte.com/' + alias2 = 'openid_hack' + self.aliases.addAlias(uri2, alias2) + + uris = ax.toTypeURIs(self.aliases, ','.join([alias1, alias2])) + self.failUnlessEqual([uri1, uri2], uris) + +class ParseAXValuesTest(unittest.TestCase): + """Testing AXKeyValueMessage.parseExtensionArgs.""" + + def failUnlessAXKeyError(self, ax_args): + msg = ax.AXKeyValueMessage() + self.failUnlessRaises(KeyError, msg.parseExtensionArgs, ax_args) + + def failUnlessAXValues(self, ax_args, expected_args): + """Fail unless parseExtensionArgs(ax_args) == expected_args.""" + msg = ax.AXKeyValueMessage() + msg.parseExtensionArgs(ax_args) + self.failUnlessEqual(expected_args, msg.data) + + def test_emptyIsValid(self): + self.failUnlessAXValues({}, {}) + + def test_missingValueForAliasExplodes(self): + self.failUnlessAXKeyError({'type.foo':'urn:foo'}) + + def test_countPresentButNotValue(self): + self.failUnlessAXKeyError({'type.foo':'urn:foo', + 'count.foo':'1'}) + + def test_invalidCountValue(self): + msg = ax.FetchRequest() + self.failUnlessRaises(ax.AXError, + msg.parseExtensionArgs, + {'type.foo':'urn:foo', + 'count.foo':'bogus'}) + + def test_requestUnlimitedValues(self): + msg = ax.FetchRequest() + + msg.parseExtensionArgs( + {'mode':'fetch_request', + 'required':'foo', + 'type.foo':'urn:foo', + 'count.foo':ax.UNLIMITED_VALUES}) + + attrs = list(msg.iterAttrs()) + foo = attrs[0] + + self.failUnless(foo.count == ax.UNLIMITED_VALUES) + self.failUnless(foo.wantsUnlimitedValues()) + + def test_longAlias(self): + # Spec minimum length is 32 characters. This is a silly test + # for this library, but it's here for completeness. + alias = 'x' * ax.MINIMUM_SUPPORTED_ALIAS_LENGTH + + msg = ax.AXKeyValueMessage() + msg.parseExtensionArgs( + {'type.%s' % (alias,): 'urn:foo', + 'count.%s' % (alias,): '1', + 'value.%s.1' % (alias,): 'first'} + ) + + def test_invalidAlias(self): + types = [ + ax.AXKeyValueMessage, + ax.FetchRequest + ] + + inputs = [ + {'type.a.b':'urn:foo', + 'count.a.b':'1'}, + {'type.a,b':'urn:foo', + 'count.a,b':'1'}, + ] + + for typ in types: + for input in inputs: + msg = typ() + self.failUnlessRaises(ax.AXError, msg.parseExtensionArgs, + input) + + def test_countPresentAndIsZero(self): + self.failUnlessAXValues( + {'type.foo':'urn:foo', + 'count.foo':'0', + }, {'urn:foo':[]}) + + def test_singletonEmpty(self): + self.failUnlessAXValues( + {'type.foo':'urn:foo', + 'value.foo':'', + }, {'urn:foo':[]}) + + def test_doubleAlias(self): + self.failUnlessAXKeyError( + {'type.foo':'urn:foo', + 'value.foo':'', + 'type.bar':'urn:foo', + 'value.bar':'', + }) + + def test_doubleSingleton(self): + self.failUnlessAXValues( + {'type.foo':'urn:foo', + 'value.foo':'', + 'type.bar':'urn:bar', + 'value.bar':'', + }, {'urn:foo':[], 'urn:bar':[]}) + + def test_singletonValue(self): + self.failUnlessAXValues( + {'type.foo':'urn:foo', + 'value.foo':'Westfall', + }, {'urn:foo':['Westfall']}) + + +class FetchRequestTest(unittest.TestCase): + def setUp(self): + self.msg = ax.FetchRequest() + self.type_a = 'http://janrain.example.com/a' + self.alias_a = 'a' + + + def test_mode(self): + self.failUnlessEqual(self.msg.mode, 'fetch_request') + + def test_construct(self): + self.failUnlessEqual({}, self.msg.requested_attributes) + self.failUnlessEqual(None, self.msg.update_url) + + msg = ax.FetchRequest('hailstorm') + self.failUnlessEqual({}, msg.requested_attributes) + self.failUnlessEqual('hailstorm', msg.update_url) + + def test_add(self): + uri = 'mud://puddle' + + # Not yet added: + self.failIf(uri in self.msg) + + attr = ax.AttrInfo(uri) + self.msg.add(attr) + + # Present after adding + self.failUnless(uri in self.msg) + + def test_addTwice(self): + uri = 'lightning://storm' + + attr = ax.AttrInfo(uri) + self.msg.add(attr) + self.failUnlessRaises(KeyError, self.msg.add, attr) + + def test_getExtensionArgs_empty(self): + expected_args = { + 'mode':'fetch_request', + } + self.failUnlessEqual(expected_args, self.msg.getExtensionArgs()) + + def test_getExtensionArgs_noAlias(self): + attr = ax.AttrInfo( + type_uri = 'type://of.transportation', + ) + self.msg.add(attr) + ax_args = self.msg.getExtensionArgs() + for k, v in ax_args.iteritems(): + if v == attr.type_uri and k.startswith('type.'): + alias = k[5:] + break + else: + self.fail("Didn't find the type definition") + + self.failUnlessExtensionArgs({ + 'type.' + alias:attr.type_uri, + 'if_available':alias, + }) + + def test_getExtensionArgs_alias_if_available(self): + attr = ax.AttrInfo( + type_uri = 'type://of.transportation', + alias = 'transport', + ) + self.msg.add(attr) + self.failUnlessExtensionArgs({ + 'type.' + attr.alias:attr.type_uri, + 'if_available':attr.alias, + }) + + def test_getExtensionArgs_alias_req(self): + attr = ax.AttrInfo( + type_uri = 'type://of.transportation', + alias = 'transport', + required = True, + ) + self.msg.add(attr) + self.failUnlessExtensionArgs({ + 'type.' + attr.alias:attr.type_uri, + 'required':attr.alias, + }) + + def failUnlessExtensionArgs(self, expected_args): + """Make sure that getExtensionArgs has the expected result + + This method will fill in the mode. + """ + expected_args = dict(expected_args) + expected_args['mode'] = self.msg.mode + self.failUnlessEqual(expected_args, self.msg.getExtensionArgs()) + + def test_isIterable(self): + self.failUnlessEqual([], list(self.msg)) + self.failUnlessEqual([], list(self.msg.iterAttrs())) + + def test_getRequiredAttrs_empty(self): + self.failUnlessEqual([], self.msg.getRequiredAttrs()) + + def test_parseExtensionArgs_extraType(self): + extension_args = { + 'mode':'fetch_request', + 'type.' + self.alias_a:self.type_a, + } + self.failUnlessRaises(ValueError, + self.msg.parseExtensionArgs, extension_args) + + def test_parseExtensionArgs(self): + extension_args = { + 'mode':'fetch_request', + 'type.' + self.alias_a:self.type_a, + 'if_available':self.alias_a + } + self.msg.parseExtensionArgs(extension_args) + self.failUnless(self.type_a in self.msg) + self.failUnlessEqual([self.type_a], list(self.msg)) + attr_info = self.msg.requested_attributes.get(self.type_a) + self.failUnless(attr_info) + self.failIf(attr_info.required) + self.failUnlessEqual(self.type_a, attr_info.type_uri) + self.failUnlessEqual(self.alias_a, attr_info.alias) + self.failUnlessEqual([attr_info], list(self.msg.iterAttrs())) + + def test_extensionArgs_idempotent(self): + extension_args = { + 'mode':'fetch_request', + 'type.' + self.alias_a:self.type_a, + 'if_available':self.alias_a + } + self.msg.parseExtensionArgs(extension_args) + self.failUnlessEqual(extension_args, self.msg.getExtensionArgs()) + self.failIf(self.msg.requested_attributes[self.type_a].required) + + def test_extensionArgs_idempotent_count_required(self): + extension_args = { + 'mode':'fetch_request', + 'type.' + self.alias_a:self.type_a, + 'count.' + self.alias_a:'2', + 'required':self.alias_a + } + self.msg.parseExtensionArgs(extension_args) + self.failUnlessEqual(extension_args, self.msg.getExtensionArgs()) + self.failUnless(self.msg.requested_attributes[self.type_a].required) + + def test_extensionArgs_count1(self): + extension_args = { + 'mode':'fetch_request', + 'type.' + self.alias_a:self.type_a, + 'count.' + self.alias_a:'1', + 'if_available':self.alias_a, + } + extension_args_norm = { + 'mode':'fetch_request', + 'type.' + self.alias_a:self.type_a, + 'if_available':self.alias_a, + } + self.msg.parseExtensionArgs(extension_args) + self.failUnlessEqual(extension_args_norm, self.msg.getExtensionArgs()) + + def test_openidNoRealm(self): + openid_req_msg = Message.fromOpenIDArgs({ + 'mode': 'checkid_setup', + 'ns': OPENID2_NS, + 'ns.ax': ax.AXMessage.ns_uri, + 'ax.update_url': 'http://different.site/path', + 'ax.mode': 'fetch_request', + }) + self.failUnlessRaises(ax.AXError, + ax.FetchRequest.fromOpenIDRequest, + DummyRequest(openid_req_msg)) + + def test_openidUpdateURLVerificationError(self): + openid_req_msg = Message.fromOpenIDArgs({ + 'mode': 'checkid_setup', + 'ns': OPENID2_NS, + 'realm': 'http://example.com/realm', + 'ns.ax': ax.AXMessage.ns_uri, + 'ax.update_url': 'http://different.site/path', + 'ax.mode': 'fetch_request', + }) + + self.failUnlessRaises(ax.AXError, + ax.FetchRequest.fromOpenIDRequest, + DummyRequest(openid_req_msg)) + + def test_openidUpdateURLVerificationSuccess(self): + openid_req_msg = Message.fromOpenIDArgs({ + 'mode': 'checkid_setup', + 'ns': OPENID2_NS, + 'realm': 'http://example.com/realm', + 'ns.ax': ax.AXMessage.ns_uri, + 'ax.update_url': 'http://example.com/realm/update_path', + 'ax.mode': 'fetch_request', + }) + + fr = ax.FetchRequest.fromOpenIDRequest(DummyRequest(openid_req_msg)) + + def test_openidUpdateURLVerificationSuccessReturnTo(self): + openid_req_msg = Message.fromOpenIDArgs({ + 'mode': 'checkid_setup', + 'ns': OPENID2_NS, + 'return_to': 'http://example.com/realm', + 'ns.ax': ax.AXMessage.ns_uri, + 'ax.update_url': 'http://example.com/realm/update_path', + 'ax.mode': 'fetch_request', + }) + + fr = ax.FetchRequest.fromOpenIDRequest(DummyRequest(openid_req_msg)) + + def test_fromOpenIDRequestWithoutExtension(self): + """return None for an OpenIDRequest without AX paramaters.""" + openid_req_msg = Message.fromOpenIDArgs({ + 'mode': 'checkid_setup', + 'ns': OPENID2_NS, + }) + oreq = DummyRequest(openid_req_msg) + r = ax.FetchRequest.fromOpenIDRequest(oreq) + self.failUnless(r is None, "%s is not None" % (r,)) + + def test_fromOpenIDRequestWithoutData(self): + """return something for SuccessResponse with AX paramaters, + even if it is the empty set.""" + openid_req_msg = Message.fromOpenIDArgs({ + 'mode': 'checkid_setup', + 'realm': 'http://example.com/realm', + 'ns': OPENID2_NS, + 'ns.ax': ax.AXMessage.ns_uri, + 'ax.mode': 'fetch_request', + }) + oreq = DummyRequest(openid_req_msg) + r = ax.FetchRequest.fromOpenIDRequest(oreq) + self.failUnless(r is not None) + + +class FetchResponseTest(unittest.TestCase): + def setUp(self): + self.msg = ax.FetchResponse() + self.value_a = 'monkeys' + self.type_a = 'http://phone.home/' + self.alias_a = 'robocop' + self.request_update_url = 'http://update.bogus/' + + def test_construct(self): + self.failUnless(self.msg.update_url is None) + self.failUnlessEqual({}, self.msg.data) + + def test_getExtensionArgs_empty(self): + expected_args = { + 'mode':'fetch_response', + } + self.failUnlessEqual(expected_args, self.msg.getExtensionArgs()) + + def test_getExtensionArgs_empty_request(self): + expected_args = { + 'mode':'fetch_response', + } + req = ax.FetchRequest() + msg = ax.FetchResponse(request=req) + self.failUnlessEqual(expected_args, msg.getExtensionArgs()) + + def test_getExtensionArgs_empty_request_some(self): + uri = 'http://not.found/' + alias = 'ext0' + + expected_args = { + 'mode':'fetch_response', + 'type.%s' % (alias,): uri, + 'count.%s' % (alias,): '0' + } + req = ax.FetchRequest() + req.add(ax.AttrInfo(uri)) + msg = ax.FetchResponse(request=req) + self.failUnlessEqual(expected_args, msg.getExtensionArgs()) + + def test_updateUrlInResponse(self): + uri = 'http://not.found/' + alias = 'ext0' + + expected_args = { + 'mode':'fetch_response', + 'update_url': self.request_update_url, + 'type.%s' % (alias,): uri, + 'count.%s' % (alias,): '0' + } + req = ax.FetchRequest(update_url=self.request_update_url) + req.add(ax.AttrInfo(uri)) + msg = ax.FetchResponse(request=req) + self.failUnlessEqual(expected_args, msg.getExtensionArgs()) + + def test_getExtensionArgs_some_request(self): + expected_args = { + 'mode':'fetch_response', + 'type.' + self.alias_a:self.type_a, + 'value.' + self.alias_a + '.1':self.value_a, + 'count.' + self.alias_a: '1' + } + req = ax.FetchRequest() + req.add(ax.AttrInfo(self.type_a, alias=self.alias_a)) + msg = ax.FetchResponse(request=req) + msg.addValue(self.type_a, self.value_a) + self.failUnlessEqual(expected_args, msg.getExtensionArgs()) + + def test_getExtensionArgs_some_not_request(self): + req = ax.FetchRequest() + msg = ax.FetchResponse(request=req) + msg.addValue(self.type_a, self.value_a) + self.failUnlessRaises(KeyError, msg.getExtensionArgs) + + def test_getSingle_success(self): + req = ax.FetchRequest() + self.msg.addValue(self.type_a, self.value_a) + self.failUnlessEqual(self.value_a, self.msg.getSingle(self.type_a)) + + def test_getSingle_none(self): + self.failUnlessEqual(None, self.msg.getSingle(self.type_a)) + + def test_getSingle_extra(self): + self.msg.setValues(self.type_a, ['x', 'y']) + self.failUnlessRaises(ax.AXError, self.msg.getSingle, self.type_a) + + def test_get(self): + self.failUnlessRaises(KeyError, self.msg.get, self.type_a) + + def test_fromSuccessResponseWithoutExtension(self): + """return None for SuccessResponse with no AX paramaters.""" + args = { + 'mode': 'id_res', + 'ns': OPENID2_NS, + } + sf = ['openid.' + i for i in args.keys()] + msg = Message.fromOpenIDArgs(args) + class Endpoint: + claimed_id = 'http://invalid.' + + oreq = SuccessResponse(Endpoint(), msg, signed_fields=sf) + r = ax.FetchResponse.fromSuccessResponse(oreq) + self.failUnless(r is None, "%s is not None" % (r,)) + + def test_fromSuccessResponseWithoutData(self): + """return something for SuccessResponse with AX paramaters, + even if it is the empty set.""" + args = { + 'mode': 'id_res', + 'ns': OPENID2_NS, + 'ns.ax': ax.AXMessage.ns_uri, + 'ax.mode': 'fetch_response', + } + sf = ['openid.' + i for i in args.keys()] + msg = Message.fromOpenIDArgs(args) + class Endpoint: + claimed_id = 'http://invalid.' + + oreq = SuccessResponse(Endpoint(), msg, signed_fields=sf) + r = ax.FetchResponse.fromSuccessResponse(oreq) + self.failUnless(r is not None) + + def test_fromSuccessResponseWithData(self): + name = 'ext0' + value = 'snozzberry' + uri = "http://willy.wonka.name/" + args = { + 'mode': 'id_res', + 'ns': OPENID2_NS, + 'ns.ax': ax.AXMessage.ns_uri, + 'ax.update_url': 'http://example.com/realm/update_path', + 'ax.mode': 'fetch_response', + 'ax.type.'+name: uri, + 'ax.count.'+name: '1', + 'ax.value.%s.1'%name: value, + } + sf = ['openid.' + i for i in args.keys()] + msg = Message.fromOpenIDArgs(args) + class Endpoint: + claimed_id = 'http://invalid.' + + resp = SuccessResponse(Endpoint(), msg, signed_fields=sf) + ax_resp = ax.FetchResponse.fromSuccessResponse(resp) + values = ax_resp.get(uri) + self.failUnlessEqual([value], values) + + +class StoreRequestTest(unittest.TestCase): + def setUp(self): + self.msg = ax.StoreRequest() + self.type_a = 'http://three.count/' + self.alias_a = 'juggling' + + def test_construct(self): + self.failUnlessEqual({}, self.msg.data) + + def test_getExtensionArgs_empty(self): + args = self.msg.getExtensionArgs() + expected_args = { + 'mode':'store_request', + } + self.failUnlessEqual(expected_args, args) + + def test_getExtensionArgs_nonempty(self): + aliases = NamespaceMap() + aliases.addAlias(self.type_a, self.alias_a) + msg = ax.StoreRequest(aliases=aliases) + msg.setValues(self.type_a, ['foo', 'bar']) + args = msg.getExtensionArgs() + expected_args = { + 'mode':'store_request', + 'type.' + self.alias_a: self.type_a, + 'count.' + self.alias_a: '2', + 'value.%s.1' % (self.alias_a,):'foo', + 'value.%s.2' % (self.alias_a,):'bar', + } + self.failUnlessEqual(expected_args, args) + +class StoreResponseTest(unittest.TestCase): + def test_success(self): + msg = ax.StoreResponse() + self.failUnless(msg.succeeded()) + self.failIf(msg.error_message) + self.failUnlessEqual({'mode':'store_response_success'}, + msg.getExtensionArgs()) + + def test_fail_nomsg(self): + msg = ax.StoreResponse(False) + self.failIf(msg.succeeded()) + self.failIf(msg.error_message) + self.failUnlessEqual({'mode':'store_response_failure'}, + msg.getExtensionArgs()) + + def test_fail_msg(self): + reason = 'no reason, really' + msg = ax.StoreResponse(False, reason) + self.failIf(msg.succeeded()) + self.failUnlessEqual(reason, msg.error_message) + self.failUnlessEqual({'mode':'store_response_failure', + 'error':reason}, msg.getExtensionArgs()) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_consumer.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_consumer.py new file mode 100644 index 0000000..33a7564 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_consumer.py @@ -0,0 +1,2097 @@ +import urlparse +import cgi +import time +import warnings + +from openid.message import Message, OPENID_NS, OPENID2_NS, IDENTIFIER_SELECT, \ + OPENID1_NS, BARE_NS +from openid import cryptutil, dh, oidutil, kvform +from openid.store.nonce import mkNonce, split as splitNonce +from openid.consumer.discover import OpenIDServiceEndpoint, OPENID_2_0_TYPE, \ + OPENID_1_1_TYPE +from openid.consumer.consumer import \ + AuthRequest, GenericConsumer, SUCCESS, FAILURE, CANCEL, SETUP_NEEDED, \ + SuccessResponse, FailureResponse, SetupNeededResponse, CancelResponse, \ + DiffieHellmanSHA1ConsumerSession, Consumer, PlainTextConsumerSession, \ + SetupNeededError, DiffieHellmanSHA256ConsumerSession, ServerError, \ + ProtocolError, _httpResponseToMessage +from openid import association +from openid.server.server import \ + PlainTextServerSession, DiffieHellmanSHA1ServerSession +from openid.yadis.manager import Discovery +from openid.yadis.discover import DiscoveryFailure +from openid.dh import DiffieHellman + +from openid.fetchers import HTTPResponse, HTTPFetchingError +from openid import fetchers +from openid.store import memstore + +from support import CatchLogs + +assocs = [ + ('another 20-byte key.', 'Snarky'), + ('\x00' * 20, 'Zeros'), + ] + +def mkSuccess(endpoint, q): + """Convenience function to create a SuccessResponse with the given + arguments, all signed.""" + signed_list = ['openid.' + k for k in q.keys()] + return SuccessResponse(endpoint, Message.fromOpenIDArgs(q), signed_list) + +def parseQuery(qs): + q = {} + for (k, v) in cgi.parse_qsl(qs): + assert not q.has_key(k) + q[k] = v + return q + +def associate(qs, assoc_secret, assoc_handle): + """Do the server's half of the associate call, using the given + secret and handle.""" + q = parseQuery(qs) + assert q['openid.mode'] == 'associate' + assert q['openid.assoc_type'] == 'HMAC-SHA1' + reply_dict = { + 'assoc_type':'HMAC-SHA1', + 'assoc_handle':assoc_handle, + 'expires_in':'600', + } + + if q.get('openid.session_type') == 'DH-SHA1': + assert len(q) == 6 or len(q) == 4 + message = Message.fromPostArgs(q) + session = DiffieHellmanSHA1ServerSession.fromMessage(message) + reply_dict['session_type'] = 'DH-SHA1' + else: + assert len(q) == 2 + session = PlainTextServerSession.fromQuery(q) + + reply_dict.update(session.answer(assoc_secret)) + return kvform.dictToKV(reply_dict) + + +GOODSIG = "[A Good Signature]" + + +class GoodAssociation: + expiresIn = 3600 + handle = "-blah-" + + def getExpiresIn(self): + return self.expiresIn + + def checkMessageSignature(self, message): + return message.getArg(OPENID_NS, 'sig') == GOODSIG + + +class GoodAssocStore(memstore.MemoryStore): + def getAssociation(self, server_url, handle=None): + return GoodAssociation() + + +class TestFetcher(object): + def __init__(self, user_url, user_page, (assoc_secret, assoc_handle)): + self.get_responses = {user_url:self.response(user_url, 200, user_page)} + self.assoc_secret = assoc_secret + self.assoc_handle = assoc_handle + self.num_assocs = 0 + + def response(self, url, status, body): + return HTTPResponse( + final_url=url, status=status, headers={}, body=body) + + def fetch(self, url, body=None, headers=None): + if body is None: + if url in self.get_responses: + return self.get_responses[url] + else: + try: + body.index('openid.mode=associate') + except ValueError: + pass # fall through + else: + assert body.find('DH-SHA1') != -1 + response = associate( + body, self.assoc_secret, self.assoc_handle) + self.num_assocs += 1 + return self.response(url, 200, response) + + return self.response(url, 404, 'Not found') + +def makeFastConsumerSession(): + """ + Create custom DH object so tests run quickly. + """ + dh = DiffieHellman(100389557, 2) + return DiffieHellmanSHA1ConsumerSession(dh) + +def setConsumerSession(con): + con.session_types = {'DH-SHA1': makeFastConsumerSession} + +def _test_success(server_url, user_url, delegate_url, links, immediate=False): + store = memstore.MemoryStore() + if immediate: + mode = 'checkid_immediate' + else: + mode = 'checkid_setup' + + endpoint = OpenIDServiceEndpoint() + endpoint.claimed_id = user_url + endpoint.server_url = server_url + endpoint.local_id = delegate_url + endpoint.type_uris = [OPENID_1_1_TYPE] + + fetcher = TestFetcher(None, None, assocs[0]) + fetchers.setDefaultFetcher(fetcher, wrap_exceptions=False) + + def run(): + trust_root = consumer_url + + consumer = GenericConsumer(store) + setConsumerSession(consumer) + + request = consumer.begin(endpoint) + return_to = consumer_url + + m = request.getMessage(trust_root, return_to, immediate) + + redirect_url = request.redirectURL(trust_root, return_to, immediate) + + parsed = urlparse.urlparse(redirect_url) + qs = parsed[4] + q = parseQuery(qs) + new_return_to = q['openid.return_to'] + del q['openid.return_to'] + assert q == { + 'openid.mode':mode, + 'openid.identity':delegate_url, + 'openid.trust_root':trust_root, + 'openid.assoc_handle':fetcher.assoc_handle, + }, (q, user_url, delegate_url, mode) + + assert new_return_to.startswith(return_to) + assert redirect_url.startswith(server_url) + + parsed = urlparse.urlparse(new_return_to) + query = parseQuery(parsed[4]) + query.update({ + 'openid.mode':'id_res', + 'openid.return_to':new_return_to, + 'openid.identity':delegate_url, + 'openid.assoc_handle':fetcher.assoc_handle, + }) + + assoc = store.getAssociation(server_url, fetcher.assoc_handle) + + message = Message.fromPostArgs(query) + message = assoc.signMessage(message) + info = consumer.complete(message, request.endpoint, new_return_to) + assert info.status == SUCCESS, info.message + assert info.identity_url == user_url + + assert fetcher.num_assocs == 0 + run() + assert fetcher.num_assocs == 1 + + # Test that doing it again uses the existing association + run() + assert fetcher.num_assocs == 1 + + # Another association is created if we remove the existing one + store.removeAssociation(server_url, fetcher.assoc_handle) + run() + assert fetcher.num_assocs == 2 + + # Test that doing it again uses the existing association + run() + assert fetcher.num_assocs == 2 + +import unittest + +http_server_url = 'http://server.example.com/' +consumer_url = 'http://consumer.example.com/' +https_server_url = 'https://server.example.com/' + +class TestSuccess(unittest.TestCase, CatchLogs): + server_url = http_server_url + user_url = 'http://www.example.com/user.html' + delegate_url = 'http://consumer.example.com/user' + + def setUp(self): + CatchLogs.setUp(self) + self.links = '' % ( + self.server_url,) + + self.delegate_links = ('' + '') % ( + self.server_url, self.delegate_url) + + def tearDown(self): + CatchLogs.tearDown(self) + + def test_nodelegate(self): + _test_success(self.server_url, self.user_url, + self.user_url, self.links) + + def test_nodelegateImmediate(self): + _test_success(self.server_url, self.user_url, + self.user_url, self.links, True) + + def test_delegate(self): + _test_success(self.server_url, self.user_url, + self.delegate_url, self.delegate_links) + + def test_delegateImmediate(self): + _test_success(self.server_url, self.user_url, + self.delegate_url, self.delegate_links, True) + + +class TestSuccessHTTPS(TestSuccess): + server_url = https_server_url + + +class TestConstruct(unittest.TestCase): + def setUp(self): + self.store_sentinel = object() + + def test_construct(self): + oidc = GenericConsumer(self.store_sentinel) + self.failUnless(oidc.store is self.store_sentinel) + + def test_nostore(self): + self.failUnlessRaises(TypeError, GenericConsumer) + + +class TestIdRes(unittest.TestCase, CatchLogs): + consumer_class = GenericConsumer + + def setUp(self): + CatchLogs.setUp(self) + + self.store = memstore.MemoryStore() + self.consumer = self.consumer_class(self.store) + self.return_to = "nonny" + self.endpoint = OpenIDServiceEndpoint() + self.endpoint.claimed_id = self.consumer_id = "consu" + self.endpoint.server_url = self.server_url = "serlie" + self.endpoint.local_id = self.server_id = "sirod" + self.endpoint.type_uris = [OPENID_1_1_TYPE] + + def disableDiscoveryVerification(self): + """Set the discovery verification to a no-op for test cases in + which we don't care.""" + def dummyVerifyDiscover(_, endpoint): + return endpoint + self.consumer._verifyDiscoveryResults = dummyVerifyDiscover + + def disableReturnToChecking(self): + def checkReturnTo(unused1, unused2): + return True + self.consumer._checkReturnTo = checkReturnTo + complete = self.consumer.complete + def callCompleteWithoutReturnTo(message, endpoint): + return complete(message, endpoint, None) + self.consumer.complete = callCompleteWithoutReturnTo + +class TestIdResCheckSignature(TestIdRes): + def setUp(self): + TestIdRes.setUp(self) + self.assoc = GoodAssociation() + self.assoc.handle = "{not_dumb}" + self.store.storeAssociation(self.endpoint.server_url, self.assoc) + + self.message = Message.fromPostArgs({ + 'openid.mode': 'id_res', + 'openid.identity': '=example', + 'openid.sig': GOODSIG, + 'openid.assoc_handle': self.assoc.handle, + 'openid.signed': 'mode,identity,assoc_handle,signed', + 'frobboz': 'banzit', + }) + + + def test_sign(self): + # assoc_handle to assoc with good sig + self.consumer._idResCheckSignature(self.message, + self.endpoint.server_url) + + + def test_signFailsWithBadSig(self): + self.message.setArg(OPENID_NS, 'sig', 'BAD SIGNATURE') + self.failUnlessRaises( + ProtocolError, self.consumer._idResCheckSignature, + self.message, self.endpoint.server_url) + + + def test_stateless(self): + # assoc_handle missing assoc, consumer._checkAuth returns goodthings + self.message.setArg(OPENID_NS, "assoc_handle", "dumbHandle") + self.consumer._processCheckAuthResponse = ( + lambda response, server_url: True) + self.consumer._makeKVPost = lambda args, server_url: {} + self.consumer._idResCheckSignature(self.message, + self.endpoint.server_url) + + def test_statelessRaisesError(self): + # assoc_handle missing assoc, consumer._checkAuth returns goodthings + self.message.setArg(OPENID_NS, "assoc_handle", "dumbHandle") + self.consumer._checkAuth = lambda unused1, unused2: False + self.failUnlessRaises( + ProtocolError, self.consumer._idResCheckSignature, + self.message, self.endpoint.server_url) + + def test_stateless_noStore(self): + # assoc_handle missing assoc, consumer._checkAuth returns goodthings + self.message.setArg(OPENID_NS, "assoc_handle", "dumbHandle") + self.consumer.store = None + self.consumer._processCheckAuthResponse = ( + lambda response, server_url: True) + self.consumer._makeKVPost = lambda args, server_url: {} + self.consumer._idResCheckSignature(self.message, + self.endpoint.server_url) + + def test_statelessRaisesError_noStore(self): + # assoc_handle missing assoc, consumer._checkAuth returns goodthings + self.message.setArg(OPENID_NS, "assoc_handle", "dumbHandle") + self.consumer._checkAuth = lambda unused1, unused2: False + self.consumer.store = None + self.failUnlessRaises( + ProtocolError, self.consumer._idResCheckSignature, + self.message, self.endpoint.server_url) + + +class TestQueryFormat(TestIdRes): + def test_notAList(self): + # XXX: should be a Message object test, not a consumer test + + # Value should be a single string. If it's a list, it should generate + # an exception. + query = {'openid.mode': ['cancel']} + try: + r = Message.fromPostArgs(query) + except TypeError, err: + self.failUnless(str(err).find('values') != -1, err) + else: + self.fail("expected TypeError, got this instead: %s" % (r,)) + +class TestComplete(TestIdRes): + """Testing GenericConsumer.complete. + + Other TestIdRes subclasses test more specific aspects. + """ + + def test_setupNeededIdRes(self): + message = Message.fromOpenIDArgs({'mode': 'id_res'}) + setup_url_sentinel = object() + + def raiseSetupNeeded(msg): + self.failUnless(msg is message) + raise SetupNeededError(setup_url_sentinel) + + self.consumer._checkSetupNeeded = raiseSetupNeeded + + response = self.consumer.complete(message, None, None) + self.failUnlessEqual(SETUP_NEEDED, response.status) + self.failUnless(setup_url_sentinel is response.setup_url) + + def test_cancel(self): + message = Message.fromPostArgs({'openid.mode': 'cancel'}) + self.disableReturnToChecking() + r = self.consumer.complete(message, self.endpoint) + self.failUnlessEqual(r.status, CANCEL) + self.failUnless(r.identity_url == self.endpoint.claimed_id) + + def test_cancel_with_return_to(self): + message = Message.fromPostArgs({'openid.mode': 'cancel'}) + r = self.consumer.complete(message, self.endpoint, self.return_to) + self.failUnlessEqual(r.status, CANCEL) + self.failUnless(r.identity_url == self.endpoint.claimed_id) + + def test_error(self): + msg = 'an error message' + message = Message.fromPostArgs({'openid.mode': 'error', + 'openid.error': msg, + }) + self.disableReturnToChecking() + r = self.consumer.complete(message, self.endpoint) + self.failUnlessEqual(r.status, FAILURE) + self.failUnless(r.identity_url == self.endpoint.claimed_id) + self.failUnlessEqual(r.message, msg) + + def test_errorWithNoOptionalKeys(self): + msg = 'an error message' + contact = 'some contact info here' + message = Message.fromPostArgs({'openid.mode': 'error', + 'openid.error': msg, + 'openid.contact': contact, + }) + self.disableReturnToChecking() + r = self.consumer.complete(message, self.endpoint) + self.failUnlessEqual(r.status, FAILURE) + self.failUnless(r.identity_url == self.endpoint.claimed_id) + self.failUnless(r.contact == contact) + self.failUnless(r.reference is None) + self.failUnlessEqual(r.message, msg) + + def test_errorWithOptionalKeys(self): + msg = 'an error message' + contact = 'me' + reference = 'support ticket' + message = Message.fromPostArgs({'openid.mode': 'error', + 'openid.error': msg, 'openid.reference': reference, + 'openid.contact': contact, 'openid.ns': OPENID2_NS, + }) + r = self.consumer.complete(message, self.endpoint, None) + self.failUnlessEqual(r.status, FAILURE) + self.failUnless(r.identity_url == self.endpoint.claimed_id) + self.failUnless(r.contact == contact) + self.failUnless(r.reference == reference) + self.failUnlessEqual(r.message, msg) + + def test_noMode(self): + message = Message.fromPostArgs({}) + r = self.consumer.complete(message, self.endpoint, None) + self.failUnlessEqual(r.status, FAILURE) + self.failUnless(r.identity_url == self.endpoint.claimed_id) + + def test_idResMissingField(self): + # XXX - this test is passing, but not necessarily by what it + # is supposed to test for. status in FAILURE, but it's because + # *check_auth* failed, not because it's missing an arg, exactly. + message = Message.fromPostArgs({'openid.mode': 'id_res'}) + self.failUnlessRaises(ProtocolError, self.consumer._doIdRes, + message, self.endpoint, None) + + def test_idResURLMismatch(self): + class VerifiedError(Exception): pass + + def discoverAndVerify(claimed_id, _to_match_endpoints): + raise VerifiedError + + self.consumer._discoverAndVerify = discoverAndVerify + self.disableReturnToChecking() + + message = Message.fromPostArgs( + {'openid.mode': 'id_res', + 'openid.return_to': 'return_to (just anything)', + 'openid.identity': 'something wrong (not self.consumer_id)', + 'openid.assoc_handle': 'does not matter', + 'openid.sig': GOODSIG, + 'openid.signed': 'identity,return_to', + }) + self.consumer.store = GoodAssocStore() + + self.failUnlessRaises(VerifiedError, + self.consumer.complete, + message, self.endpoint) + + self.failUnlessLogMatches('Error attempting to use stored', + 'Attempting discovery') + +class TestCompleteMissingSig(unittest.TestCase, CatchLogs): + + def setUp(self): + self.store = GoodAssocStore() + self.consumer = GenericConsumer(self.store) + self.server_url = "http://idp.unittest/" + CatchLogs.setUp(self) + + claimed_id = 'bogus.claimed' + + self.message = Message.fromOpenIDArgs( + {'mode': 'id_res', + 'return_to': 'return_to (just anything)', + 'identity': claimed_id, + 'assoc_handle': 'does not matter', + 'sig': GOODSIG, + 'response_nonce': mkNonce(), + 'signed': 'identity,return_to,response_nonce,assoc_handle,claimed_id,op_endpoint', + 'claimed_id': claimed_id, + 'op_endpoint': self.server_url, + 'ns':OPENID2_NS, + }) + + self.endpoint = OpenIDServiceEndpoint() + self.endpoint.server_url = self.server_url + self.endpoint.claimed_id = claimed_id + self.consumer._checkReturnTo = lambda unused1, unused2 : True + + def tearDown(self): + CatchLogs.tearDown(self) + + + def test_idResMissingNoSigs(self): + def _vrfy(resp_msg, endpoint=None): + return endpoint + + self.consumer._verifyDiscoveryResults = _vrfy + r = self.consumer.complete(self.message, self.endpoint, None) + self.failUnlessSuccess(r) + + + def test_idResNoIdentity(self): + self.message.delArg(OPENID_NS, 'identity') + self.message.delArg(OPENID_NS, 'claimed_id') + self.endpoint.claimed_id = None + self.message.setArg(OPENID_NS, 'signed', 'return_to,response_nonce,assoc_handle,op_endpoint') + r = self.consumer.complete(self.message, self.endpoint, None) + self.failUnlessSuccess(r) + + + def test_idResMissingIdentitySig(self): + self.message.setArg(OPENID_NS, 'signed', 'return_to,response_nonce,assoc_handle,claimed_id') + r = self.consumer.complete(self.message, self.endpoint, None) + self.failUnlessEqual(r.status, FAILURE) + + + def test_idResMissingReturnToSig(self): + self.message.setArg(OPENID_NS, 'signed', 'identity,response_nonce,assoc_handle,claimed_id') + r = self.consumer.complete(self.message, self.endpoint, None) + self.failUnlessEqual(r.status, FAILURE) + + + def test_idResMissingAssocHandleSig(self): + self.message.setArg(OPENID_NS, 'signed', 'identity,response_nonce,return_to,claimed_id') + r = self.consumer.complete(self.message, self.endpoint, None) + self.failUnlessEqual(r.status, FAILURE) + + + def test_idResMissingClaimedIDSig(self): + self.message.setArg(OPENID_NS, 'signed', 'identity,response_nonce,return_to,assoc_handle') + r = self.consumer.complete(self.message, self.endpoint, None) + self.failUnlessEqual(r.status, FAILURE) + + + def failUnlessSuccess(self, response): + if response.status != SUCCESS: + self.fail("Non-successful response: %s" % (response,)) + + + +class TestCheckAuthResponse(TestIdRes, CatchLogs): + def setUp(self): + CatchLogs.setUp(self) + TestIdRes.setUp(self) + + def tearDown(self): + CatchLogs.tearDown(self) + + def _createAssoc(self): + issued = time.time() + lifetime = 1000 + assoc = association.Association( + 'handle', 'secret', issued, lifetime, 'HMAC-SHA1') + store = self.consumer.store + store.storeAssociation(self.server_url, assoc) + assoc2 = store.getAssociation(self.server_url) + self.failUnlessEqual(assoc, assoc2) + + def test_goodResponse(self): + """successful response to check_authentication""" + response = Message.fromOpenIDArgs({'is_valid':'true',}) + r = self.consumer._processCheckAuthResponse(response, self.server_url) + self.failUnless(r) + + def test_missingAnswer(self): + """check_authentication returns false when the server sends no answer""" + response = Message.fromOpenIDArgs({}) + r = self.consumer._processCheckAuthResponse(response, self.server_url) + self.failIf(r) + + def test_badResponse(self): + """check_authentication returns false when is_valid is false""" + response = Message.fromOpenIDArgs({'is_valid':'false',}) + r = self.consumer._processCheckAuthResponse(response, self.server_url) + self.failIf(r) + + def test_badResponseInvalidate(self): + """Make sure that the handle is invalidated when is_valid is false + + From "Verifying directly with the OpenID Provider":: + + If the OP responds with "is_valid" set to "true", and + "invalidate_handle" is present, the Relying Party SHOULD + NOT send further authentication requests with that handle. + """ + self._createAssoc() + response = Message.fromOpenIDArgs({ + 'is_valid':'false', + 'invalidate_handle':'handle', + }) + r = self.consumer._processCheckAuthResponse(response, self.server_url) + self.failIf(r) + self.failUnless( + self.consumer.store.getAssociation(self.server_url) is None) + + def test_invalidateMissing(self): + """invalidate_handle with a handle that is not present""" + response = Message.fromOpenIDArgs({ + 'is_valid':'true', + 'invalidate_handle':'missing', + }) + r = self.consumer._processCheckAuthResponse(response, self.server_url) + self.failUnless(r) + self.failUnlessLogMatches( + 'Received "invalidate_handle"' + ) + + def test_invalidateMissing_noStore(self): + """invalidate_handle with a handle that is not present""" + response = Message.fromOpenIDArgs({ + 'is_valid':'true', + 'invalidate_handle':'missing', + }) + self.consumer.store = None + r = self.consumer._processCheckAuthResponse(response, self.server_url) + self.failUnless(r) + self.failUnlessLogMatches( + 'Received "invalidate_handle"', + 'Unexpectedly got invalidate_handle without a store') + + def test_invalidatePresent(self): + """invalidate_handle with a handle that exists + + From "Verifying directly with the OpenID Provider":: + + If the OP responds with "is_valid" set to "true", and + "invalidate_handle" is present, the Relying Party SHOULD + NOT send further authentication requests with that handle. + """ + self._createAssoc() + response = Message.fromOpenIDArgs({ + 'is_valid':'true', + 'invalidate_handle':'handle', + }) + r = self.consumer._processCheckAuthResponse(response, self.server_url) + self.failUnless(r) + self.failUnless( + self.consumer.store.getAssociation(self.server_url) is None) + +class TestSetupNeeded(TestIdRes): + def failUnlessSetupNeeded(self, expected_setup_url, message): + try: + self.consumer._checkSetupNeeded(message) + except SetupNeededError, why: + self.failUnlessEqual(expected_setup_url, why.user_setup_url) + else: + self.fail("Expected to find an immediate-mode response") + + def test_setupNeededOpenID1(self): + """The minimum conditions necessary to trigger Setup Needed""" + setup_url = 'http://unittest/setup-here' + message = Message.fromPostArgs({ + 'openid.mode': 'id_res', + 'openid.user_setup_url': setup_url, + }) + self.failUnless(message.isOpenID1()) + self.failUnlessSetupNeeded(setup_url, message) + + def test_setupNeededOpenID1_extra(self): + """Extra stuff along with setup_url still trigger Setup Needed""" + setup_url = 'http://unittest/setup-here' + message = Message.fromPostArgs({ + 'openid.mode': 'id_res', + 'openid.user_setup_url': setup_url, + 'openid.identity': 'bogus', + }) + self.failUnless(message.isOpenID1()) + self.failUnlessSetupNeeded(setup_url, message) + + def test_noSetupNeededOpenID1(self): + """When the user_setup_url is missing on an OpenID 1 message, + we assume that it's not a cancel response to checkid_immediate""" + message = Message.fromOpenIDArgs({'mode': 'id_res'}) + self.failUnless(message.isOpenID1()) + + # No SetupNeededError raised + self.consumer._checkSetupNeeded(message) + + def test_setupNeededOpenID2(self): + message = Message.fromOpenIDArgs({ + 'mode':'setup_needed', + 'ns':OPENID2_NS, + }) + self.failUnless(message.isOpenID2()) + response = self.consumer.complete(message, None, None) + self.failUnlessEqual('setup_needed', response.status) + self.failUnlessEqual(None, response.setup_url) + + def test_setupNeededDoesntWorkForOpenID1(self): + message = Message.fromOpenIDArgs({ + 'mode':'setup_needed', + }) + + # No SetupNeededError raised + self.consumer._checkSetupNeeded(message) + + response = self.consumer.complete(message, None, None) + self.failUnlessEqual('failure', response.status) + self.failUnless(response.message.startswith('Invalid openid.mode')) + + def test_noSetupNeededOpenID2(self): + message = Message.fromOpenIDArgs({ + 'mode':'id_res', + 'game':'puerto_rico', + 'ns':OPENID2_NS, + }) + self.failUnless(message.isOpenID2()) + + # No SetupNeededError raised + self.consumer._checkSetupNeeded(message) + +class IdResCheckForFieldsTest(TestIdRes): + def setUp(self): + self.consumer = GenericConsumer(None) + + def mkSuccessTest(openid_args, signed_list): + def test(self): + message = Message.fromOpenIDArgs(openid_args) + message.setArg(OPENID_NS, 'signed', ','.join(signed_list)) + self.consumer._idResCheckForFields(message) + return test + + test_openid1Success = mkSuccessTest( + {'return_to':'return', + 'assoc_handle':'assoc handle', + 'sig':'a signature', + 'identity':'someone', + }, + ['return_to', 'identity']) + + test_openid2Success = mkSuccessTest( + {'ns':OPENID2_NS, + 'return_to':'return', + 'assoc_handle':'assoc handle', + 'sig':'a signature', + 'op_endpoint':'my favourite server', + 'response_nonce':'use only once', + }, + ['return_to', 'response_nonce', 'assoc_handle', 'op_endpoint']) + + test_openid2Success_identifiers = mkSuccessTest( + {'ns':OPENID2_NS, + 'return_to':'return', + 'assoc_handle':'assoc handle', + 'sig':'a signature', + 'claimed_id':'i claim to be me', + 'identity':'my server knows me as me', + 'op_endpoint':'my favourite server', + 'response_nonce':'use only once', + }, + ['return_to', 'response_nonce', 'identity', + 'claimed_id', 'assoc_handle', 'op_endpoint']) + + def mkMissingFieldTest(openid_args): + def test(self): + message = Message.fromOpenIDArgs(openid_args) + try: + self.consumer._idResCheckForFields(message) + except ProtocolError, why: + self.failUnless(why[0].startswith('Missing required')) + else: + self.fail('Expected an error, but none occurred') + return test + + def mkMissingSignedTest(openid_args): + def test(self): + message = Message.fromOpenIDArgs(openid_args) + try: + self.consumer._idResCheckForFields(message) + except ProtocolError, why: + self.failUnless(why[0].endswith('not signed')) + else: + self.fail('Expected an error, but none occurred') + return test + + test_openid1Missing_returnToSig = mkMissingSignedTest( + {'return_to':'return', + 'assoc_handle':'assoc handle', + 'sig':'a signature', + 'identity':'someone', + 'signed':'identity', + }) + + test_openid1Missing_identitySig = mkMissingSignedTest( + {'return_to':'return', + 'assoc_handle':'assoc handle', + 'sig':'a signature', + 'identity':'someone', + 'signed':'return_to' + }) + + test_openid2Missing_opEndpointSig = mkMissingSignedTest( + {'ns':OPENID2_NS, + 'return_to':'return', + 'assoc_handle':'assoc handle', + 'sig':'a signature', + 'identity':'someone', + 'op_endpoint':'the endpoint', + 'signed':'return_to,identity,assoc_handle' + }) + + test_openid1MissingReturnTo = mkMissingFieldTest( + {'assoc_handle':'assoc handle', + 'sig':'a signature', + 'identity':'someone', + }) + + test_openid1MissingAssocHandle = mkMissingFieldTest( + {'return_to':'return', + 'sig':'a signature', + 'identity':'someone', + }) + + # XXX: I could go on... + +class CheckAuthHappened(Exception): pass + +class CheckNonceVerifyTest(TestIdRes, CatchLogs): + def setUp(self): + CatchLogs.setUp(self) + TestIdRes.setUp(self) + self.consumer.openid1_nonce_query_arg_name = 'nonce' + + def tearDown(self): + CatchLogs.tearDown(self) + + def test_openid1Success(self): + """use consumer-generated nonce""" + nonce_value = mkNonce() + self.return_to = 'http://rt.unittest/?nonce=%s' % (nonce_value,) + self.response = Message.fromOpenIDArgs({'return_to': self.return_to}) + self.response.setArg(BARE_NS, 'nonce', nonce_value) + self.consumer._idResCheckNonce(self.response, self.endpoint) + self.failUnlessLogEmpty() + + def test_openid1Missing(self): + """use consumer-generated nonce""" + self.response = Message.fromOpenIDArgs({}) + n = self.consumer._idResGetNonceOpenID1(self.response, self.endpoint) + self.failUnless(n is None, n) + self.failUnlessLogEmpty() + + def test_consumerNonceOpenID2(self): + """OpenID 2 does not use consumer-generated nonce""" + self.return_to = 'http://rt.unittest/?nonce=%s' % (mkNonce(),) + self.response = Message.fromOpenIDArgs( + {'return_to': self.return_to, 'ns':OPENID2_NS}) + self.failUnlessRaises(ProtocolError, self.consumer._idResCheckNonce, + self.response, self.endpoint) + self.failUnlessLogEmpty() + + def test_serverNonce(self): + """use server-generated nonce""" + self.response = Message.fromOpenIDArgs( + {'ns':OPENID2_NS, 'response_nonce': mkNonce(),}) + self.consumer._idResCheckNonce(self.response, self.endpoint) + self.failUnlessLogEmpty() + + def test_serverNonceOpenID1(self): + """OpenID 1 does not use server-generated nonce""" + self.response = Message.fromOpenIDArgs( + {'ns':OPENID1_NS, + 'return_to': 'http://return.to/', + 'response_nonce': mkNonce(),}) + self.failUnlessRaises(ProtocolError, self.consumer._idResCheckNonce, + self.response, self.endpoint) + self.failUnlessLogEmpty() + + def test_badNonce(self): + """remove the nonce from the store + + From "Checking the Nonce":: + + When the Relying Party checks the signature on an assertion, the + + Relying Party SHOULD ensure that an assertion has not yet + been accepted with the same value for "openid.response_nonce" + from the same OP Endpoint URL. + """ + nonce = mkNonce() + stamp, salt = splitNonce(nonce) + self.store.useNonce(self.server_url, stamp, salt) + self.response = Message.fromOpenIDArgs( + {'response_nonce': nonce, + 'ns':OPENID2_NS, + }) + self.failUnlessRaises(ProtocolError, self.consumer._idResCheckNonce, + self.response, self.endpoint) + + def test_successWithNoStore(self): + """When there is no store, checking the nonce succeeds""" + self.consumer.store = None + self.response = Message.fromOpenIDArgs( + {'response_nonce': mkNonce(), + 'ns':OPENID2_NS, + }) + self.consumer._idResCheckNonce(self.response, self.endpoint) + self.failUnlessLogEmpty() + + def test_tamperedNonce(self): + """Malformed nonce""" + self.response = Message.fromOpenIDArgs( + {'ns':OPENID2_NS, + 'response_nonce':'malformed'}) + self.failUnlessRaises(ProtocolError, self.consumer._idResCheckNonce, + self.response, self.endpoint) + + def test_missingNonce(self): + """no nonce parameter on the return_to""" + self.response = Message.fromOpenIDArgs( + {'return_to': self.return_to}) + self.failUnlessRaises(ProtocolError, self.consumer._idResCheckNonce, + self.response, self.endpoint) + +class CheckAuthDetectingConsumer(GenericConsumer): + def _checkAuth(self, *args): + raise CheckAuthHappened(args) + + def _idResCheckNonce(self, *args): + """We're not testing nonce-checking, so just return success + when it asks.""" + return True + +class TestCheckAuthTriggered(TestIdRes, CatchLogs): + consumer_class = CheckAuthDetectingConsumer + + def setUp(self): + TestIdRes.setUp(self) + CatchLogs.setUp(self) + self.disableDiscoveryVerification() + + def test_checkAuthTriggered(self): + message = Message.fromPostArgs({ + 'openid.return_to':self.return_to, + 'openid.identity':self.server_id, + 'openid.assoc_handle':'not_found', + 'openid.sig': GOODSIG, + 'openid.signed': 'identity,return_to', + }) + self.disableReturnToChecking() + try: + result = self.consumer._doIdRes(message, self.endpoint, None) + except CheckAuthHappened: + pass + else: + self.fail('_checkAuth did not happen. Result was: %r %s' % + (result, self.messages)) + + def test_checkAuthTriggeredWithAssoc(self): + # Store an association for this server that does not match the + # handle that is in the message + issued = time.time() + lifetime = 1000 + assoc = association.Association( + 'handle', 'secret', issued, lifetime, 'HMAC-SHA1') + self.store.storeAssociation(self.server_url, assoc) + self.disableReturnToChecking() + message = Message.fromPostArgs({ + 'openid.return_to':self.return_to, + 'openid.identity':self.server_id, + 'openid.assoc_handle':'not_found', + 'openid.sig': GOODSIG, + 'openid.signed': 'identity,return_to', + }) + try: + result = self.consumer._doIdRes(message, self.endpoint, None) + except CheckAuthHappened: + pass + else: + self.fail('_checkAuth did not happen. Result was: %r' % (result,)) + + def test_expiredAssoc(self): + # Store an expired association for the server with the handle + # that is in the message + issued = time.time() - 10 + lifetime = 0 + handle = 'handle' + assoc = association.Association( + handle, 'secret', issued, lifetime, 'HMAC-SHA1') + self.failUnless(assoc.expiresIn <= 0) + self.store.storeAssociation(self.server_url, assoc) + + message = Message.fromPostArgs({ + 'openid.return_to':self.return_to, + 'openid.identity':self.server_id, + 'openid.assoc_handle':handle, + 'openid.sig': GOODSIG, + 'openid.signed': 'identity,return_to', + }) + self.disableReturnToChecking() + self.failUnlessRaises(ProtocolError, self.consumer._doIdRes, + message, self.endpoint, None) + + def test_newerAssoc(self): + lifetime = 1000 + + good_issued = time.time() - 10 + good_handle = 'handle' + good_assoc = association.Association( + good_handle, 'secret', good_issued, lifetime, 'HMAC-SHA1') + self.store.storeAssociation(self.server_url, good_assoc) + + bad_issued = time.time() - 5 + bad_handle = 'handle2' + bad_assoc = association.Association( + bad_handle, 'secret', bad_issued, lifetime, 'HMAC-SHA1') + self.store.storeAssociation(self.server_url, bad_assoc) + + query = { + 'return_to':self.return_to, + 'identity':self.server_id, + 'assoc_handle':good_handle, + } + + message = Message.fromOpenIDArgs(query) + message = good_assoc.signMessage(message) + self.disableReturnToChecking() + info = self.consumer._doIdRes(message, self.endpoint, None) + self.failUnlessEqual(info.status, SUCCESS, info.message) + self.failUnlessEqual(self.consumer_id, info.identity_url) + + + +class TestReturnToArgs(unittest.TestCase): + """Verifying the Return URL paramaters. + From the specification "Verifying the Return URL":: + + To verify that the "openid.return_to" URL matches the URL that is + processing this assertion: + + - The URL scheme, authority, and path MUST be the same between the + two URLs. + + - Any query parameters that are present in the "openid.return_to" + URL MUST also be present with the same values in the + accepting URL. + + XXX: So far we have only tested the second item on the list above. + XXX: _verifyReturnToArgs is not invoked anywhere. + """ + + def setUp(self): + store = object() + self.consumer = GenericConsumer(store) + + def test_returnToArgsOkay(self): + query = { + 'openid.mode': 'id_res', + 'openid.return_to': 'http://example.com/?foo=bar', + 'foo': 'bar', + } + # no return value, success is assumed if there are no exceptions. + self.consumer._verifyReturnToArgs(query) + + def test_returnToArgsUnexpectedArg(self): + query = { + 'openid.mode': 'id_res', + 'openid.return_to': 'http://example.com/', + 'foo': 'bar', + } + # no return value, success is assumed if there are no exceptions. + self.failUnlessRaises(ProtocolError, + self.consumer._verifyReturnToArgs, query) + + def test_returnToMismatch(self): + query = { + 'openid.mode': 'id_res', + 'openid.return_to': 'http://example.com/?foo=bar', + } + # fail, query has no key 'foo'. + self.failUnlessRaises(ValueError, + self.consumer._verifyReturnToArgs, query) + + query['foo'] = 'baz' + # fail, values for 'foo' do not match. + self.failUnlessRaises(ValueError, + self.consumer._verifyReturnToArgs, query) + + + def test_noReturnTo(self): + query = {'openid.mode': 'id_res'} + self.failUnlessRaises(ValueError, + self.consumer._verifyReturnToArgs, query) + + def test_completeBadReturnTo(self): + """Test GenericConsumer.complete()'s handling of bad return_to + values. + """ + return_to = "http://some.url/path?foo=bar" + + # Scheme, authority, and path differences are checked by + # GenericConsumer._checkReturnTo. Query args checked by + # GenericConsumer._verifyReturnToArgs. + bad_return_tos = [ + # Scheme only + "https://some.url/path?foo=bar", + # Authority only + "http://some.url.invalid/path?foo=bar", + # Path only + "http://some.url/path_extra?foo=bar", + # Query args differ + "http://some.url/path?foo=bar2", + "http://some.url/path?foo2=bar", + ] + + m = Message(OPENID1_NS) + m.setArg(OPENID_NS, 'mode', 'cancel') + m.setArg(BARE_NS, 'foo', 'bar') + endpoint = None + + for bad in bad_return_tos: + m.setArg(OPENID_NS, 'return_to', bad) + self.failIf(self.consumer._checkReturnTo(m, return_to)) + + def test_completeGoodReturnTo(self): + """Test GenericConsumer.complete()'s handling of good + return_to values. + """ + return_to = "http://some.url/path" + + good_return_tos = [ + (return_to, {}), + (return_to + "?another=arg", {(BARE_NS, 'another'): 'arg'}), + (return_to + "?another=arg#fragment", {(BARE_NS, 'another'): 'arg'}), + ("HTTP"+return_to[4:], {}), + (return_to.replace('url','URL'), {}), + ("http://some.url:80/path", {}), + ("http://some.url/p%61th", {}), + ("http://some.url/./path", {}), + ] + + endpoint = None + + for good, extra in good_return_tos: + m = Message(OPENID1_NS) + m.setArg(OPENID_NS, 'mode', 'cancel') + + for ns, key in extra: + m.setArg(ns, key, extra[(ns, key)]) + + m.setArg(OPENID_NS, 'return_to', good) + result = self.consumer.complete(m, endpoint, return_to) + self.failUnless(isinstance(result, CancelResponse), \ + "Expected CancelResponse, got %r for %s" % (result, good,)) + +class MockFetcher(object): + def __init__(self, response=None): + self.response = response or HTTPResponse() + self.fetches = [] + + def fetch(self, url, body=None, headers=None): + self.fetches.append((url, body, headers)) + return self.response + +class ExceptionRaisingMockFetcher(object): + class MyException(Exception): + pass + + def fetch(self, url, body=None, headers=None): + raise self.MyException('mock fetcher exception') + +class BadArgCheckingConsumer(GenericConsumer): + def _makeKVPost(self, args, _): + assert args == { + 'openid.mode':'check_authentication', + 'openid.signed':'foo', + 'openid.ns':OPENID1_NS + }, args + return None + +class TestCheckAuth(unittest.TestCase, CatchLogs): + consumer_class = GenericConsumer + + def setUp(self): + CatchLogs.setUp(self) + self.store = memstore.MemoryStore() + + self.consumer = self.consumer_class(self.store) + + self._orig_fetcher = fetchers.getDefaultFetcher() + self.fetcher = MockFetcher() + fetchers.setDefaultFetcher(self.fetcher) + + def tearDown(self): + CatchLogs.tearDown(self) + fetchers.setDefaultFetcher(self._orig_fetcher, wrap_exceptions=False) + + def test_error(self): + self.fetcher.response = HTTPResponse( + "http://some_url", 404, {'Hea': 'der'}, 'blah:blah\n') + query = {'openid.signed': 'stuff', + 'openid.stuff':'a value'} + r = self.consumer._checkAuth(Message.fromPostArgs(query), + http_server_url) + self.failIf(r) + self.failUnless(self.messages) + + def test_bad_args(self): + query = { + 'openid.signed':'foo', + 'closid.foo':'something', + } + consumer = BadArgCheckingConsumer(self.store) + consumer._checkAuth(Message.fromPostArgs(query), 'does://not.matter') + + + def test_signedList(self): + query = Message.fromOpenIDArgs({ + 'mode': 'id_res', + 'sig': 'rabbits', + 'identity': '=example', + 'assoc_handle': 'munchkins', + 'ns.sreg': 'urn:sreg', + 'sreg.email': 'bogus@example.com', + 'signed': 'identity,mode,ns.sreg,sreg.email', + 'foo': 'bar', + }) + args = self.consumer._createCheckAuthRequest(query) + self.failUnless(args.isOpenID1()) + for signed_arg in query.getArg(OPENID_NS, 'signed').split(','): + self.failUnless(args.getAliasedArg(signed_arg), signed_arg) + + def test_112(self): + args = {'openid.assoc_handle': 'fa1f5ff0-cde4-11dc-a183-3714bfd55ca8', + 'openid.claimed_id': 'http://binkley.lan/user/test01', + 'openid.identity': 'http://test01.binkley.lan/', + 'openid.mode': 'id_res', + 'openid.ns': 'http://specs.openid.net/auth/2.0', + 'openid.ns.pape': 'http://specs.openid.net/extensions/pape/1.0', + 'openid.op_endpoint': 'http://binkley.lan/server', + 'openid.pape.auth_policies': 'none', + 'openid.pape.auth_time': '2008-01-28T20:42:36Z', + 'openid.pape.nist_auth_level': '0', + 'openid.response_nonce': '2008-01-28T21:07:04Z99Q=', + 'openid.return_to': 'http://binkley.lan:8001/process?janrain_nonce=2008-01-28T21%3A07%3A02Z0tMIKx', + 'openid.sig': 'YJlWH4U6SroB1HoPkmEKx9AyGGg=', + 'openid.signed': 'assoc_handle,identity,response_nonce,return_to,claimed_id,op_endpoint,pape.auth_time,ns.pape,pape.nist_auth_level,pape.auth_policies' + } + self.failUnlessEqual(OPENID2_NS, args['openid.ns']) + incoming = Message.fromPostArgs(args) + self.failUnless(incoming.isOpenID2()) + car = self.consumer._createCheckAuthRequest(incoming) + expected_args = args.copy() + expected_args['openid.mode'] = 'check_authentication' + expected =Message.fromPostArgs(expected_args) + self.failUnless(expected.isOpenID2()) + self.failUnlessEqual(expected, car) + self.failUnlessEqual(expected_args, car.toPostArgs()) + + + +class TestFetchAssoc(unittest.TestCase, CatchLogs): + consumer_class = GenericConsumer + + def setUp(self): + CatchLogs.setUp(self) + self.store = memstore.MemoryStore() + self.fetcher = MockFetcher() + fetchers.setDefaultFetcher(self.fetcher) + self.consumer = self.consumer_class(self.store) + + def test_error_404(self): + """404 from a kv post raises HTTPFetchingError""" + self.fetcher.response = HTTPResponse( + "http://some_url", 404, {'Hea': 'der'}, 'blah:blah\n') + self.failUnlessRaises( + fetchers.HTTPFetchingError, + self.consumer._makeKVPost, + Message.fromPostArgs({'mode':'associate'}), + "http://server_url") + + def test_error_exception_unwrapped(self): + """Ensure that exceptions are bubbled through from fetchers + when making associations + """ + self.fetcher = ExceptionRaisingMockFetcher() + fetchers.setDefaultFetcher(self.fetcher, wrap_exceptions=False) + self.failUnlessRaises(self.fetcher.MyException, + self.consumer._makeKVPost, + Message.fromPostArgs({'mode':'associate'}), + "http://server_url") + + # exception fetching returns no association + e = OpenIDServiceEndpoint() + e.server_url = 'some://url' + self.failUnlessRaises(self.fetcher.MyException, + self.consumer._getAssociation, e) + + self.failUnlessRaises(self.fetcher.MyException, + self.consumer._checkAuth, + Message.fromPostArgs({'openid.signed':''}), + 'some://url') + + def test_error_exception_wrapped(self): + """Ensure that openid.fetchers.HTTPFetchingError is caught by + the association creation stuff. + """ + self.fetcher = ExceptionRaisingMockFetcher() + # This will wrap exceptions! + fetchers.setDefaultFetcher(self.fetcher) + self.failUnlessRaises(fetchers.HTTPFetchingError, + self.consumer._makeKVPost, + Message.fromOpenIDArgs({'mode':'associate'}), + "http://server_url") + + # exception fetching returns no association + e = OpenIDServiceEndpoint() + e.server_url = 'some://url' + self.failUnless(self.consumer._getAssociation(e) is None) + + msg = Message.fromPostArgs({'openid.signed':''}) + self.failIf(self.consumer._checkAuth(msg, 'some://url')) + + +class TestSuccessResponse(unittest.TestCase): + def setUp(self): + self.endpoint = OpenIDServiceEndpoint() + self.endpoint.claimed_id = 'identity_url' + + def test_extensionResponse(self): + resp = mkSuccess(self.endpoint, { + 'ns.sreg':'urn:sreg', + 'ns.unittest':'urn:unittest', + 'unittest.one':'1', + 'unittest.two':'2', + 'sreg.nickname':'j3h', + 'return_to':'return_to', + }) + utargs = resp.extensionResponse('urn:unittest', False) + self.failUnlessEqual(utargs, {'one':'1', 'two':'2'}) + sregargs = resp.extensionResponse('urn:sreg', False) + self.failUnlessEqual(sregargs, {'nickname':'j3h'}) + + def test_extensionResponseSigned(self): + args = { + 'ns.sreg':'urn:sreg', + 'ns.unittest':'urn:unittest', + 'unittest.one':'1', + 'unittest.two':'2', + 'sreg.nickname':'j3h', + 'sreg.dob':'yesterday', + 'return_to':'return_to', + 'signed': 'sreg.nickname,unittest.one,sreg.dob', + } + + signed_list = ['openid.sreg.nickname', + 'openid.unittest.one', + 'openid.sreg.dob',] + + # Don't use mkSuccess because it creates an all-inclusive + # signed list. + msg = Message.fromOpenIDArgs(args) + resp = SuccessResponse(self.endpoint, msg, signed_list) + + # All args in this NS are signed, so expect all. + sregargs = resp.extensionResponse('urn:sreg', True) + self.failUnlessEqual(sregargs, {'nickname':'j3h', 'dob': 'yesterday'}) + + # Not all args in this NS are signed, so expect None when + # asking for them. + utargs = resp.extensionResponse('urn:unittest', True) + self.failUnlessEqual(utargs, None) + + def test_noReturnTo(self): + resp = mkSuccess(self.endpoint, {}) + self.failUnless(resp.getReturnTo() is None) + + def test_returnTo(self): + resp = mkSuccess(self.endpoint, {'return_to':'return_to'}) + self.failUnlessEqual(resp.getReturnTo(), 'return_to') + + def test_displayIdentifierClaimedId(self): + resp = mkSuccess(self.endpoint, {}) + self.failUnlessEqual(resp.getDisplayIdentifier(), + resp.endpoint.claimed_id) + + def test_displayIdentifierOverride(self): + self.endpoint.display_identifier = "http://input.url/" + resp = mkSuccess(self.endpoint, {}) + self.failUnlessEqual(resp.getDisplayIdentifier(), + "http://input.url/") + +class StubConsumer(object): + def __init__(self): + self.assoc = object() + self.response = None + self.endpoint = None + + def begin(self, service): + auth_req = AuthRequest(service, self.assoc) + self.endpoint = service + return auth_req + + def complete(self, message, endpoint, return_to): + assert endpoint is self.endpoint + return self.response + +class ConsumerTest(unittest.TestCase): + """Tests for high-level consumer.Consumer functions. + + Its GenericConsumer component is stubbed out with StubConsumer. + """ + def setUp(self): + self.endpoint = OpenIDServiceEndpoint() + self.endpoint.claimed_id = self.identity_url = 'http://identity.url/' + self.store = None + self.session = {} + self.consumer = Consumer(self.session, self.store) + self.consumer.consumer = StubConsumer() + self.discovery = Discovery(self.session, + self.identity_url, + self.consumer.session_key_prefix) + + def test_setAssociationPreference(self): + self.consumer.setAssociationPreference([]) + self.failUnless(isinstance(self.consumer.consumer.negotiator, + association.SessionNegotiator)) + self.failUnlessEqual([], + self.consumer.consumer.negotiator.allowed_types) + self.consumer.setAssociationPreference([('HMAC-SHA1', 'DH-SHA1')]) + self.failUnlessEqual([('HMAC-SHA1', 'DH-SHA1')], + self.consumer.consumer.negotiator.allowed_types) + + def withDummyDiscovery(self, callable, dummy_getNextService): + class DummyDisco(object): + def __init__(self, *ignored): + pass + + getNextService = dummy_getNextService + + import openid.consumer.consumer + old_discovery = openid.consumer.consumer.Discovery + try: + openid.consumer.consumer.Discovery = DummyDisco + callable() + finally: + openid.consumer.consumer.Discovery = old_discovery + + def test_beginHTTPError(self): + """Make sure that the discovery HTTP failure case behaves properly + """ + def getNextService(self, ignored): + raise HTTPFetchingError("Unit test") + + def test(): + try: + self.consumer.begin('unused in this test') + except DiscoveryFailure, why: + self.failUnless(why[0].startswith('Error fetching')) + self.failIf(why[0].find('Unit test') == -1) + else: + self.fail('Expected DiscoveryFailure') + + self.withDummyDiscovery(test, getNextService) + + def test_beginNoServices(self): + def getNextService(self, ignored): + return None + + url = 'http://a.user.url/' + def test(): + try: + self.consumer.begin(url) + except DiscoveryFailure, why: + self.failUnless(why[0].startswith('No usable OpenID')) + self.failIf(why[0].find(url) == -1) + else: + self.fail('Expected DiscoveryFailure') + + self.withDummyDiscovery(test, getNextService) + + + def test_beginWithoutDiscovery(self): + # Does this really test anything non-trivial? + result = self.consumer.beginWithoutDiscovery(self.endpoint) + + # The result is an auth request + self.failUnless(isinstance(result, AuthRequest)) + + # Side-effect of calling beginWithoutDiscovery is setting the + # session value to the endpoint attribute of the result + self.failUnless(self.session[self.consumer._token_key] is result.endpoint) + + # The endpoint that we passed in is the endpoint on the auth_request + self.failUnless(result.endpoint is self.endpoint) + + def test_completeEmptySession(self): + text = "failed complete" + + def checkEndpoint(message, endpoint, return_to): + self.failUnless(endpoint is None) + return FailureResponse(endpoint, text) + + self.consumer.consumer.complete = checkEndpoint + + response = self.consumer.complete({}, None) + self.failUnlessEqual(response.status, FAILURE) + self.failUnlessEqual(response.message, text) + self.failUnless(response.identity_url is None) + + def _doResp(self, auth_req, exp_resp): + """complete a transaction, using the expected response from + the generic consumer.""" + # response is an attribute of StubConsumer, returned by + # StubConsumer.complete. + self.consumer.consumer.response = exp_resp + + # endpoint is stored in the session + self.failUnless(self.session) + resp = self.consumer.complete({}, None) + + # All responses should have the same identity URL, and the + # session should be cleaned out + if self.endpoint.claimed_id != IDENTIFIER_SELECT: + self.failUnless(resp.identity_url is self.identity_url) + + self.failIf(self.consumer._token_key in self.session) + + # Expected status response + self.failUnlessEqual(resp.status, exp_resp.status) + + return resp + + def _doRespNoDisco(self, exp_resp): + """Set up a transaction without discovery""" + auth_req = self.consumer.beginWithoutDiscovery(self.endpoint) + resp = self._doResp(auth_req, exp_resp) + # There should be nothing left in the session once we have completed. + self.failIf(self.session) + return resp + + def test_noDiscoCompleteSuccessWithToken(self): + self._doRespNoDisco(mkSuccess(self.endpoint, {})) + + def test_noDiscoCompleteCancelWithToken(self): + self._doRespNoDisco(CancelResponse(self.endpoint)) + + def test_noDiscoCompleteFailure(self): + msg = 'failed!' + resp = self._doRespNoDisco(FailureResponse(self.endpoint, msg)) + self.failUnless(resp.message is msg) + + def test_noDiscoCompleteSetupNeeded(self): + setup_url = 'http://setup.url/' + resp = self._doRespNoDisco( + SetupNeededResponse(self.endpoint, setup_url)) + self.failUnless(resp.setup_url is setup_url) + + # To test that discovery is cleaned up, we need to initialize a + # Yadis manager, and have it put its values in the session. + def _doRespDisco(self, is_clean, exp_resp): + """Set up and execute a transaction, with discovery""" + self.discovery.createManager([self.endpoint], self.identity_url) + auth_req = self.consumer.begin(self.identity_url) + resp = self._doResp(auth_req, exp_resp) + + manager = self.discovery.getManager() + if is_clean: + self.failUnless(self.discovery.getManager() is None, manager) + else: + self.failIf(self.discovery.getManager() is None, manager) + + return resp + + # Cancel and success DO clean up the discovery process + def test_completeSuccess(self): + self._doRespDisco(True, mkSuccess(self.endpoint, {})) + + def test_completeCancel(self): + self._doRespDisco(True, CancelResponse(self.endpoint)) + + # Failure and setup_needed don't clean up the discovery process + def test_completeFailure(self): + msg = 'failed!' + resp = self._doRespDisco(False, FailureResponse(self.endpoint, msg)) + self.failUnless(resp.message is msg) + + def test_completeSetupNeeded(self): + setup_url = 'http://setup.url/' + resp = self._doRespDisco( + False, + SetupNeededResponse(self.endpoint, setup_url)) + self.failUnless(resp.setup_url is setup_url) + + def test_successDifferentURL(self): + """ + Be sure that the session gets cleaned up when the response is + successful and has a different URL than the one in the + request. + """ + # Set up a request endpoint describing an IDP URL + self.identity_url = 'http://idp.url/' + self.endpoint.claimed_id = self.endpoint.local_id = IDENTIFIER_SELECT + + # Use a response endpoint with a different URL (asserted by + # the IDP) + resp_endpoint = OpenIDServiceEndpoint() + resp_endpoint.claimed_id = "http://user.url/" + + resp = self._doRespDisco( + True, + mkSuccess(resp_endpoint, {})) + self.failUnless(self.discovery.getManager(force=True) is None) + + def test_begin(self): + self.discovery.createManager([self.endpoint], self.identity_url) + # Should not raise an exception + auth_req = self.consumer.begin(self.identity_url) + self.failUnless(isinstance(auth_req, AuthRequest)) + self.failUnless(auth_req.endpoint is self.endpoint) + self.failUnless(auth_req.endpoint is self.consumer.consumer.endpoint) + self.failUnless(auth_req.assoc is self.consumer.consumer.assoc) + + + +class IDPDrivenTest(unittest.TestCase): + + def setUp(self): + self.store = GoodAssocStore() + self.consumer = GenericConsumer(self.store) + self.endpoint = OpenIDServiceEndpoint() + self.endpoint.server_url = "http://idp.unittest/" + + + def test_idpDrivenBegin(self): + # Testing here that the token-handling doesn't explode... + self.consumer.begin(self.endpoint) + + + def test_idpDrivenComplete(self): + identifier = '=directed_identifier' + message = Message.fromPostArgs({ + 'openid.identity': '=directed_identifier', + 'openid.return_to': 'x', + 'openid.assoc_handle': 'z', + 'openid.signed': 'identity,return_to', + 'openid.sig': GOODSIG, + }) + + discovered_endpoint = OpenIDServiceEndpoint() + discovered_endpoint.claimed_id = identifier + discovered_endpoint.server_url = self.endpoint.server_url + discovered_endpoint.local_id = identifier + iverified = [] + def verifyDiscoveryResults(identifier, endpoint): + self.failUnless(endpoint is self.endpoint) + iverified.append(discovered_endpoint) + return discovered_endpoint + self.consumer._verifyDiscoveryResults = verifyDiscoveryResults + self.consumer._idResCheckNonce = lambda *args: True + self.consumer._checkReturnTo = lambda unused1, unused2 : True + response = self.consumer._doIdRes(message, self.endpoint, None) + + self.failUnlessSuccess(response) + self.failUnlessEqual(response.identity_url, "=directed_identifier") + + # assert that discovery attempt happens and returns good + self.failUnlessEqual(iverified, [discovered_endpoint]) + + + def test_idpDrivenCompleteFraud(self): + # crap with an identifier that doesn't match discovery info + message = Message.fromPostArgs({ + 'openid.identity': '=directed_identifier', + 'openid.return_to': 'x', + 'openid.assoc_handle': 'z', + 'openid.signed': 'identity,return_to', + 'openid.sig': GOODSIG, + }) + def verifyDiscoveryResults(identifier, endpoint): + raise DiscoveryFailure("PHREAK!", None) + self.consumer._verifyDiscoveryResults = verifyDiscoveryResults + self.consumer._checkReturnTo = lambda unused1, unused2 : True + self.failUnlessRaises(DiscoveryFailure, self.consumer._doIdRes, + message, self.endpoint, None) + + + def failUnlessSuccess(self, response): + if response.status != SUCCESS: + self.fail("Non-successful response: %s" % (response,)) + + + +class TestDiscoveryVerification(unittest.TestCase): + services = [] + + def setUp(self): + self.store = GoodAssocStore() + self.consumer = GenericConsumer(self.store) + + self.consumer._discover = self.discoveryFunc + + self.identifier = "http://idp.unittest/1337" + self.server_url = "http://endpoint.unittest/" + + self.message = Message.fromPostArgs({ + 'openid.ns': OPENID2_NS, + 'openid.identity': self.identifier, + 'openid.claimed_id': self.identifier, + 'openid.op_endpoint': self.server_url, + }) + + self.endpoint = OpenIDServiceEndpoint() + self.endpoint.server_url = self.server_url + + def test_theGoodStuff(self): + endpoint = OpenIDServiceEndpoint() + endpoint.type_uris = [OPENID_2_0_TYPE] + endpoint.claimed_id = self.identifier + endpoint.server_url = self.server_url + endpoint.local_id = self.identifier + self.services = [endpoint] + r = self.consumer._verifyDiscoveryResults(self.message, endpoint) + + self.failUnlessEqual(r, endpoint) + + + def test_otherServer(self): + text = "verify failed" + + def discoverAndVerify(claimed_id, to_match_endpoints): + self.failUnlessEqual(claimed_id, self.identifier) + for to_match in to_match_endpoints: + self.failUnlessEqual(claimed_id, to_match.claimed_id) + raise ProtocolError(text) + + self.consumer._discoverAndVerify = discoverAndVerify + + # a set of things without the stuff + endpoint = OpenIDServiceEndpoint() + endpoint.type_uris = [OPENID_2_0_TYPE] + endpoint.claimed_id = self.identifier + endpoint.server_url = "http://the-MOON.unittest/" + endpoint.local_id = self.identifier + self.services = [endpoint] + try: + r = self.consumer._verifyDiscoveryResults(self.message, endpoint) + except ProtocolError, e: + # Should we make more ProtocolError subclasses? + self.failUnless(str(e), text) + else: + self.fail("expected ProtocolError, %r returned." % (r,)) + + + def test_foreignDelegate(self): + text = "verify failed" + + def discoverAndVerify(claimed_id, to_match_endpoints): + self.failUnlessEqual(claimed_id, self.identifier) + for to_match in to_match_endpoints: + self.failUnlessEqual(claimed_id, to_match.claimed_id) + raise ProtocolError(text) + + self.consumer._discoverAndVerify = discoverAndVerify + + # a set of things with the server stuff but other delegate + endpoint = OpenIDServiceEndpoint() + endpoint.type_uris = [OPENID_2_0_TYPE] + endpoint.claimed_id = self.identifier + endpoint.server_url = self.server_url + endpoint.local_id = "http://unittest/juan-carlos" + + try: + r = self.consumer._verifyDiscoveryResults(self.message, endpoint) + except ProtocolError, e: + self.failUnlessEqual(str(e), text) + else: + self.fail("Exepected ProtocolError, %r returned" % (r,)) + + def test_nothingDiscovered(self): + # a set of no things. + self.services = [] + self.failUnlessRaises(DiscoveryFailure, + self.consumer._verifyDiscoveryResults, + self.message, self.endpoint) + + + def discoveryFunc(self, identifier): + return identifier, self.services + + +class TestCreateAssociationRequest(unittest.TestCase): + def setUp(self): + class DummyEndpoint(object): + use_compatibility = False + + def compatibilityMode(self): + return self.use_compatibility + + self.endpoint = DummyEndpoint() + self.consumer = GenericConsumer(store=None) + self.assoc_type = 'HMAC-SHA1' + + def test_noEncryptionSendsType(self): + session_type = 'no-encryption' + session, args = self.consumer._createAssociateRequest( + self.endpoint, self.assoc_type, session_type) + + self.failUnless(isinstance(session, PlainTextConsumerSession)) + expected = Message.fromOpenIDArgs( + {'ns':OPENID2_NS, + 'session_type':session_type, + 'mode':'associate', + 'assoc_type':self.assoc_type, + }) + + self.failUnlessEqual(expected, args) + + def test_noEncryptionCompatibility(self): + self.endpoint.use_compatibility = True + session_type = 'no-encryption' + session, args = self.consumer._createAssociateRequest( + self.endpoint, self.assoc_type, session_type) + + self.failUnless(isinstance(session, PlainTextConsumerSession)) + self.failUnlessEqual(Message.fromOpenIDArgs({'mode':'associate', + 'assoc_type':self.assoc_type, + }), args) + + def test_dhSHA1Compatibility(self): + # Set the consumer's session type to a fast session since we + # need it here. + setConsumerSession(self.consumer) + + self.endpoint.use_compatibility = True + session_type = 'DH-SHA1' + session, args = self.consumer._createAssociateRequest( + self.endpoint, self.assoc_type, session_type) + + self.failUnless(isinstance(session, DiffieHellmanSHA1ConsumerSession)) + + # This is a random base-64 value, so just check that it's + # present. + self.failUnless(args.getArg(OPENID1_NS, 'dh_consumer_public')) + args.delArg(OPENID1_NS, 'dh_consumer_public') + + # OK, session_type is set here and not for no-encryption + # compatibility + expected = Message.fromOpenIDArgs({'mode':'associate', + 'session_type':'DH-SHA1', + 'assoc_type':self.assoc_type, + 'dh_modulus': 'BfvStQ==', + 'dh_gen': 'Ag==', + }) + + self.failUnlessEqual(expected, args) + + # XXX: test the other types + +class TestDiffieHellmanResponseParameters(object): + session_cls = None + message_namespace = None + + def setUp(self): + # Pre-compute DH with small prime so tests run quickly. + self.server_dh = DiffieHellman(100389557, 2) + self.consumer_dh = DiffieHellman(100389557, 2) + + # base64(btwoc(g ^ xb mod p)) + self.dh_server_public = cryptutil.longToBase64(self.server_dh.public) + + self.secret = cryptutil.randomString(self.session_cls.secret_size) + + self.enc_mac_key = oidutil.toBase64( + self.server_dh.xorSecret(self.consumer_dh.public, + self.secret, + self.session_cls.hash_func)) + + self.consumer_session = self.session_cls(self.consumer_dh) + + self.msg = Message(self.message_namespace) + + def testExtractSecret(self): + self.msg.setArg(OPENID_NS, 'dh_server_public', self.dh_server_public) + self.msg.setArg(OPENID_NS, 'enc_mac_key', self.enc_mac_key) + + extracted = self.consumer_session.extractSecret(self.msg) + self.failUnlessEqual(extracted, self.secret) + + def testAbsentServerPublic(self): + self.msg.setArg(OPENID_NS, 'enc_mac_key', self.enc_mac_key) + + self.failUnlessRaises(KeyError, self.consumer_session.extractSecret, self.msg) + + def testAbsentMacKey(self): + self.msg.setArg(OPENID_NS, 'dh_server_public', self.dh_server_public) + + self.failUnlessRaises(KeyError, self.consumer_session.extractSecret, self.msg) + + def testInvalidBase64Public(self): + self.msg.setArg(OPENID_NS, 'dh_server_public', 'n o t b a s e 6 4.') + self.msg.setArg(OPENID_NS, 'enc_mac_key', self.enc_mac_key) + + self.failUnlessRaises(ValueError, self.consumer_session.extractSecret, self.msg) + + def testInvalidBase64MacKey(self): + self.msg.setArg(OPENID_NS, 'dh_server_public', self.dh_server_public) + self.msg.setArg(OPENID_NS, 'enc_mac_key', 'n o t base 64') + + self.failUnlessRaises(ValueError, self.consumer_session.extractSecret, self.msg) + +class TestOpenID1SHA1(TestDiffieHellmanResponseParameters, unittest.TestCase): + session_cls = DiffieHellmanSHA1ConsumerSession + message_namespace = OPENID1_NS + +class TestOpenID2SHA1(TestDiffieHellmanResponseParameters, unittest.TestCase): + session_cls = DiffieHellmanSHA1ConsumerSession + message_namespace = OPENID2_NS + +if cryptutil.SHA256_AVAILABLE: + class TestOpenID2SHA256(TestDiffieHellmanResponseParameters, unittest.TestCase): + session_cls = DiffieHellmanSHA256ConsumerSession + message_namespace = OPENID2_NS +else: + warnings.warn("Not running SHA256 association session tests.") + +class TestNoStore(unittest.TestCase): + def setUp(self): + self.consumer = GenericConsumer(None) + + def test_completeNoGetAssoc(self): + """_getAssociation is never called when the store is None""" + def notCalled(unused): + self.fail('This method was unexpectedly called') + + endpoint = OpenIDServiceEndpoint() + endpoint.claimed_id = 'identity_url' + + self.consumer._getAssociation = notCalled + auth_request = self.consumer.begin(endpoint) + # _getAssociation was not called + + + + +class NonAnonymousAuthRequest(object): + endpoint = 'unused' + + def setAnonymous(self, unused): + raise ValueError('Should trigger ProtocolError') + +class TestConsumerAnonymous(unittest.TestCase): + def test_beginWithoutDiscoveryAnonymousFail(self): + """Make sure that ValueError for setting an auth request + anonymous gets converted to a ProtocolError + """ + sess = {} + consumer = Consumer(sess, None) + def bogusBegin(unused): + return NonAnonymousAuthRequest() + consumer.consumer.begin = bogusBegin + self.failUnlessRaises( + ProtocolError, + consumer.beginWithoutDiscovery, None) + + +class TestDiscoverAndVerify(unittest.TestCase): + def setUp(self): + self.consumer = GenericConsumer(None) + self.discovery_result = None + def dummyDiscover(unused_identifier): + return self.discovery_result + self.consumer._discover = dummyDiscover + self.to_match = OpenIDServiceEndpoint() + + def failUnlessDiscoveryFailure(self): + self.failUnlessRaises( + DiscoveryFailure, + self.consumer._discoverAndVerify, + 'http://claimed-id.com/', + [self.to_match]) + + def test_noServices(self): + """Discovery returning no results results in a + DiscoveryFailure exception""" + self.discovery_result = (None, []) + self.failUnlessDiscoveryFailure() + + def test_noMatches(self): + """If no discovered endpoint matches the values from the + assertion, then we end up raising a ProtocolError + """ + self.discovery_result = (None, ['unused']) + def raiseProtocolError(unused1, unused2): + raise ProtocolError('unit test') + self.consumer._verifyDiscoverySingle = raiseProtocolError + self.failUnlessDiscoveryFailure() + + def test_matches(self): + """If an endpoint matches, we return it + """ + # Discovery returns a single "endpoint" object + matching_endpoint = 'matching endpoint' + self.discovery_result = (None, [matching_endpoint]) + + # Make verifying discovery return True for this endpoint + def returnTrue(unused1, unused2): + return True + self.consumer._verifyDiscoverySingle = returnTrue + + # Since _verifyDiscoverySingle returns True, we should get the + # first endpoint that we passed in as a result. + result = self.consumer._discoverAndVerify( + 'http://claimed.id/', [self.to_match]) + self.failUnlessEqual(matching_endpoint, result) + +from openid.extension import Extension +class SillyExtension(Extension): + ns_uri = 'http://silly.example.com/' + ns_alias = 'silly' + + def getExtensionArgs(self): + return {'i_am':'silly'} + +class TestAddExtension(unittest.TestCase): + + def test_SillyExtension(self): + ext = SillyExtension() + ar = AuthRequest(OpenIDServiceEndpoint(), None) + ar.addExtension(ext) + ext_args = ar.message.getArgs(ext.ns_uri) + self.failUnlessEqual(ext.getExtensionArgs(), ext_args) + + + +class TestKVPost(unittest.TestCase): + def setUp(self): + self.server_url = 'http://unittest/%s' % (self.id(),) + + def test_200(self): + from openid.fetchers import HTTPResponse + response = HTTPResponse() + response.status = 200 + response.body = "foo:bar\nbaz:quux\n" + r = _httpResponseToMessage(response, self.server_url) + expected_msg = Message.fromOpenIDArgs({'foo':'bar','baz':'quux'}) + self.failUnlessEqual(expected_msg, r) + + + def test_400(self): + response = HTTPResponse() + response.status = 400 + response.body = "error:bonk\nerror_code:7\n" + try: + r = _httpResponseToMessage(response, self.server_url) + except ServerError, e: + self.failUnlessEqual(e.error_text, 'bonk') + self.failUnlessEqual(e.error_code, '7') + else: + self.fail("Expected ServerError, got return %r" % (r,)) + + + def test_500(self): + # 500 as an example of any non-200, non-400 code. + response = HTTPResponse() + response.status = 500 + response.body = "foo:bar\nbaz:quux\n" + self.failUnlessRaises(fetchers.HTTPFetchingError, + _httpResponseToMessage, response, + self.server_url) + + + + +if __name__ == '__main__': + unittest.main() diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_discover.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_discover.py new file mode 100644 index 0000000..fca9363 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_discover.py @@ -0,0 +1,783 @@ +import sys +import unittest +import datadriven +import os.path +from openid import fetchers +from openid.fetchers import HTTPResponse +from openid.yadis.discover import DiscoveryFailure +from openid.consumer import discover +from openid.yadis import xrires +from openid.yadis.xri import XRI +from urlparse import urlsplit +from openid import message + +### Tests for conditions that trigger DiscoveryFailure + +class SimpleMockFetcher(object): + def __init__(self, responses): + self.responses = list(responses) + + def fetch(self, url, body=None, headers=None): + response = self.responses.pop(0) + assert body is None + assert response.final_url == url + return response + +class TestDiscoveryFailure(datadriven.DataDrivenTestCase): + cases = [ + [HTTPResponse('http://network.error/', None)], + [HTTPResponse('http://not.found/', 404)], + [HTTPResponse('http://bad.request/', 400)], + [HTTPResponse('http://server.error/', 500)], + [HTTPResponse('http://header.found/', 200, + headers={'x-xrds-location':'http://xrds.missing/'}), + HTTPResponse('http://xrds.missing/', 404)], + ] + + def __init__(self, responses): + self.url = responses[0].final_url + datadriven.DataDrivenTestCase.__init__(self, self.url) + self.responses = responses + + def setUp(self): + fetcher = SimpleMockFetcher(self.responses) + fetchers.setDefaultFetcher(fetcher) + + def tearDown(self): + fetchers.setDefaultFetcher(None) + + def runOneTest(self): + expected_status = self.responses[-1].status + try: + discover.discover(self.url) + except DiscoveryFailure, why: + self.failUnlessEqual(why.http_response.status, expected_status) + else: + self.fail('Did not raise DiscoveryFailure') + + +### Tests for raising/catching exceptions from the fetcher through the +### discover function + +# Python 2.5 displays a message when running this test, which is +# testing the behaviour in the presence of string exceptions, +# deprecated or not, so tell it no to complain when this particular +# string exception is raised. +import warnings +warnings.filterwarnings('ignore', 'raising a string.*', DeprecationWarning, + r'^openid\.test\.test_discover$', 77) + +class ErrorRaisingFetcher(object): + """Just raise an exception when fetch is called""" + + def __init__(self, thing_to_raise): + self.thing_to_raise = thing_to_raise + + def fetch(self, url, body=None, headers=None): + raise self.thing_to_raise + +class DidFetch(Exception): + """Custom exception just to make sure it's not handled differently""" + +class TestFetchException(datadriven.DataDrivenTestCase): + """Make sure exceptions get passed through discover function from + fetcher.""" + + cases = [ + Exception(), + DidFetch(), + ValueError(), + RuntimeError(), + ] + + # String exceptions are finally gone from Python 2.6. + if sys.version_info[:2] < (2, 6): + cases.append('oi!') + + def __init__(self, exc): + datadriven.DataDrivenTestCase.__init__(self, repr(exc)) + self.exc = exc + + def setUp(self): + fetcher = ErrorRaisingFetcher(self.exc) + fetchers.setDefaultFetcher(fetcher, wrap_exceptions=False) + + def tearDown(self): + fetchers.setDefaultFetcher(None) + + def runOneTest(self): + try: + discover.discover('http://doesnt.matter/') + except: + exc = sys.exc_info()[1] + if exc is None: + # str exception + self.failUnless(self.exc is sys.exc_info()[0]) + else: + self.failUnless(self.exc is exc, exc) + else: + self.fail('Expected %r', self.exc) + + +### Tests for openid.consumer.discover.discover + +class TestNormalization(unittest.TestCase): + def testAddingProtocol(self): + f = ErrorRaisingFetcher(RuntimeError()) + fetchers.setDefaultFetcher(f, wrap_exceptions=False) + + try: + discover.discover('users.stompy.janrain.com:8000/x') + except DiscoveryFailure, why: + self.fail('failed to parse url with port correctly') + except RuntimeError: + pass #expected + + fetchers.setDefaultFetcher(None) + + +class DiscoveryMockFetcher(object): + redirect = None + + def __init__(self, documents): + self.documents = documents + self.fetchlog = [] + + def fetch(self, url, body=None, headers=None): + self.fetchlog.append((url, body, headers)) + if self.redirect: + final_url = self.redirect + else: + final_url = url + + try: + ctype, body = self.documents[url] + except KeyError: + status = 404 + ctype = 'text/plain' + body = '' + else: + status = 200 + + return HTTPResponse(final_url, status, {'content-type': ctype}, body) + +# from twisted.trial import unittest as trialtest + +class BaseTestDiscovery(unittest.TestCase): + id_url = "http://someuser.unittest/" + + documents = {} + fetcherClass = DiscoveryMockFetcher + + def _checkService(self, s, + server_url, + claimed_id=None, + local_id=None, + canonical_id=None, + types=None, + used_yadis=False, + display_identifier=None + ): + self.failUnlessEqual(server_url, s.server_url) + if types == ['2.0 OP']: + self.failIf(claimed_id) + self.failIf(local_id) + self.failIf(s.claimed_id) + self.failIf(s.local_id) + self.failIf(s.getLocalID()) + self.failIf(s.compatibilityMode()) + self.failUnless(s.isOPIdentifier()) + self.failUnlessEqual(s.preferredNamespace(), + discover.OPENID_2_0_MESSAGE_NS) + else: + self.failUnlessEqual(claimed_id, s.claimed_id) + self.failUnlessEqual(local_id, s.getLocalID()) + + if used_yadis: + self.failUnless(s.used_yadis, "Expected to use Yadis") + else: + self.failIf(s.used_yadis, + "Expected to use old-style discovery") + + openid_types = { + '1.1': discover.OPENID_1_1_TYPE, + '1.0': discover.OPENID_1_0_TYPE, + '2.0': discover.OPENID_2_0_TYPE, + '2.0 OP': discover.OPENID_IDP_2_0_TYPE, + } + + type_uris = [openid_types[t] for t in types] + self.failUnlessEqual(type_uris, s.type_uris) + self.failUnlessEqual(canonical_id, s.canonicalID) + + if s.canonicalID: + self.failUnless(s.getDisplayIdentifier() != claimed_id) + self.failUnless(s.getDisplayIdentifier() is not None) + self.failUnlessEqual(display_identifier, s.getDisplayIdentifier()) + self.failUnlessEqual(s.claimed_id, s.canonicalID) + + self.failUnlessEqual(s.display_identifier or s.claimed_id, s.getDisplayIdentifier()) + + def setUp(self): + self.documents = self.documents.copy() + self.fetcher = self.fetcherClass(self.documents) + fetchers.setDefaultFetcher(self.fetcher) + + def tearDown(self): + fetchers.setDefaultFetcher(None) + +def readDataFile(filename): + module_directory = os.path.dirname(os.path.abspath(__file__)) + filename = os.path.join( + module_directory, 'data', 'test_discover', filename) + return file(filename).read() + +class TestDiscovery(BaseTestDiscovery): + def _discover(self, content_type, data, + expected_services, expected_id=None): + if expected_id is None: + expected_id = self.id_url + + self.documents[self.id_url] = (content_type, data) + id_url, services = discover.discover(self.id_url) + self.failUnlessEqual(expected_services, len(services)) + self.failUnlessEqual(expected_id, id_url) + return services + + def test_404(self): + self.failUnlessRaises(DiscoveryFailure, + discover.discover, self.id_url + '/404') + + def test_noOpenID(self): + services = self._discover(content_type='text/plain', + data="junk", + expected_services=0) + + services = self._discover( + content_type='text/html', + data=readDataFile('openid_no_delegate.html'), + expected_services=1, + ) + + self._checkService( + services[0], + used_yadis=False, + types=['1.1'], + server_url="http://www.myopenid.com/server", + claimed_id=self.id_url, + local_id=self.id_url, + ) + + def test_html1(self): + services = self._discover( + content_type='text/html', + data=readDataFile('openid.html'), + expected_services=1) + + + self._checkService( + services[0], + used_yadis=False, + types=['1.1'], + server_url="http://www.myopenid.com/server", + claimed_id=self.id_url, + local_id='http://smoker.myopenid.com/', + display_identifier=self.id_url, + ) + + def test_html1Fragment(self): + """Ensure that the Claimed Identifier does not have a fragment + if one is supplied in the User Input.""" + content_type = 'text/html' + data = readDataFile('openid.html') + expected_services = 1 + + self.documents[self.id_url] = (content_type, data) + expected_id = self.id_url + self.id_url = self.id_url + '#fragment' + id_url, services = discover.discover(self.id_url) + self.failUnlessEqual(expected_services, len(services)) + self.failUnlessEqual(expected_id, id_url) + + self._checkService( + services[0], + used_yadis=False, + types=['1.1'], + server_url="http://www.myopenid.com/server", + claimed_id=expected_id, + local_id='http://smoker.myopenid.com/', + display_identifier=expected_id, + ) + + def test_html2(self): + services = self._discover( + content_type='text/html', + data=readDataFile('openid2.html'), + expected_services=1, + ) + + self._checkService( + services[0], + used_yadis=False, + types=['2.0'], + server_url="http://www.myopenid.com/server", + claimed_id=self.id_url, + local_id='http://smoker.myopenid.com/', + display_identifier=self.id_url, + ) + + def test_html1And2(self): + services = self._discover( + content_type='text/html', + data=readDataFile('openid_1_and_2.html'), + expected_services=2, + ) + + for t, s in zip(['2.0', '1.1'], services): + self._checkService( + s, + used_yadis=False, + types=[t], + server_url="http://www.myopenid.com/server", + claimed_id=self.id_url, + local_id='http://smoker.myopenid.com/', + display_identifier=self.id_url, + ) + + def test_yadisEmpty(self): + services = self._discover(content_type='application/xrds+xml', + data=readDataFile('yadis_0entries.xml'), + expected_services=0) + + def test_htmlEmptyYadis(self): + """HTML document has discovery information, but points to an + empty Yadis document.""" + # The XRDS document pointed to by "openid_and_yadis.html" + self.documents[self.id_url + 'xrds'] = ( + 'application/xrds+xml', readDataFile('yadis_0entries.xml')) + + services = self._discover(content_type='text/html', + data=readDataFile('openid_and_yadis.html'), + expected_services=1) + + self._checkService( + services[0], + used_yadis=False, + types=['1.1'], + server_url="http://www.myopenid.com/server", + claimed_id=self.id_url, + local_id='http://smoker.myopenid.com/', + display_identifier=self.id_url, + ) + + def test_yadis1NoDelegate(self): + services = self._discover(content_type='application/xrds+xml', + data=readDataFile('yadis_no_delegate.xml'), + expected_services=1) + + self._checkService( + services[0], + used_yadis=True, + types=['1.0'], + server_url="http://www.myopenid.com/server", + claimed_id=self.id_url, + local_id=self.id_url, + display_identifier=self.id_url, + ) + + def test_yadis2NoLocalID(self): + services = self._discover( + content_type='application/xrds+xml', + data=readDataFile('openid2_xrds_no_local_id.xml'), + expected_services=1, + ) + + self._checkService( + services[0], + used_yadis=True, + types=['2.0'], + server_url="http://www.myopenid.com/server", + claimed_id=self.id_url, + local_id=self.id_url, + display_identifier=self.id_url, + ) + + def test_yadis2(self): + services = self._discover( + content_type='application/xrds+xml', + data=readDataFile('openid2_xrds.xml'), + expected_services=1, + ) + + self._checkService( + services[0], + used_yadis=True, + types=['2.0'], + server_url="http://www.myopenid.com/server", + claimed_id=self.id_url, + local_id='http://smoker.myopenid.com/', + display_identifier=self.id_url, + ) + + def test_yadis2OP(self): + services = self._discover( + content_type='application/xrds+xml', + data=readDataFile('yadis_idp.xml'), + expected_services=1, + ) + + self._checkService( + services[0], + used_yadis=True, + types=['2.0 OP'], + server_url="http://www.myopenid.com/server", + display_identifier=self.id_url, + ) + + def test_yadis2OPDelegate(self): + """The delegate tag isn't meaningful for OP entries.""" + services = self._discover( + content_type='application/xrds+xml', + data=readDataFile('yadis_idp_delegate.xml'), + expected_services=1, + ) + + self._checkService( + services[0], + used_yadis=True, + types=['2.0 OP'], + server_url="http://www.myopenid.com/server", + display_identifier=self.id_url, + ) + + def test_yadis2BadLocalID(self): + self.failUnlessRaises(DiscoveryFailure, self._discover, + content_type='application/xrds+xml', + data=readDataFile('yadis_2_bad_local_id.xml'), + expected_services=1, + ) + + def test_yadis1And2(self): + services = self._discover( + content_type='application/xrds+xml', + data=readDataFile('openid_1_and_2_xrds.xml'), + expected_services=1, + ) + + self._checkService( + services[0], + used_yadis=True, + types=['2.0', '1.1'], + server_url="http://www.myopenid.com/server", + claimed_id=self.id_url, + local_id='http://smoker.myopenid.com/', + display_identifier=self.id_url, + ) + + def test_yadis1And2BadLocalID(self): + self.failUnlessRaises(DiscoveryFailure, self._discover, + content_type='application/xrds+xml', + data=readDataFile('openid_1_and_2_xrds_bad_delegate.xml'), + expected_services=1, + ) + +class MockFetcherForXRIProxy(object): + + def __init__(self, documents, proxy_url=xrires.DEFAULT_PROXY): + self.documents = documents + self.fetchlog = [] + self.proxy_url = None + + + def fetch(self, url, body=None, headers=None): + self.fetchlog.append((url, body, headers)) + + u = urlsplit(url) + proxy_host = u[1] + xri = u[2] + query = u[3] + + if not headers and not query: + raise ValueError("No headers or query; you probably didn't " + "mean to do that.") + + if xri.startswith('/'): + xri = xri[1:] + + try: + ctype, body = self.documents[xri] + except KeyError: + status = 404 + ctype = 'text/plain' + body = '' + else: + status = 200 + + return HTTPResponse(url, status, {'content-type': ctype}, body) + + +class TestXRIDiscovery(BaseTestDiscovery): + fetcherClass = MockFetcherForXRIProxy + + documents = {'=smoker': ('application/xrds+xml', + readDataFile('yadis_2entries_delegate.xml')), + '=smoker*bad': ('application/xrds+xml', + readDataFile('yadis_another_delegate.xml')) } + + def test_xri(self): + user_xri, services = discover.discoverXRI('=smoker') + + self._checkService( + services[0], + used_yadis=True, + types=['1.0'], + server_url="http://www.myopenid.com/server", + claimed_id=XRI("=!1000"), + canonical_id=XRI("=!1000"), + local_id='http://smoker.myopenid.com/', + display_identifier='=smoker' + ) + + self._checkService( + services[1], + used_yadis=True, + types=['1.0'], + server_url="http://www.livejournal.com/openid/server.bml", + claimed_id=XRI("=!1000"), + canonical_id=XRI("=!1000"), + local_id='http://frank.livejournal.com/', + display_identifier='=smoker' + ) + + def test_xri_normalize(self): + user_xri, services = discover.discoverXRI('xri://=smoker') + + self._checkService( + services[0], + used_yadis=True, + types=['1.0'], + server_url="http://www.myopenid.com/server", + claimed_id=XRI("=!1000"), + canonical_id=XRI("=!1000"), + local_id='http://smoker.myopenid.com/', + display_identifier='=smoker' + ) + + self._checkService( + services[1], + used_yadis=True, + types=['1.0'], + server_url="http://www.livejournal.com/openid/server.bml", + claimed_id=XRI("=!1000"), + canonical_id=XRI("=!1000"), + local_id='http://frank.livejournal.com/', + display_identifier='=smoker' + ) + + def test_xriNoCanonicalID(self): + user_xri, services = discover.discoverXRI('=smoker*bad') + self.failIf(services) + + def test_useCanonicalID(self): + """When there is no delegate, the CanonicalID should be used with XRI. + """ + endpoint = discover.OpenIDServiceEndpoint() + endpoint.claimed_id = XRI("=!1000") + endpoint.canonicalID = XRI("=!1000") + self.failUnlessEqual(endpoint.getLocalID(), XRI("=!1000")) + + +class TestXRIDiscoveryIDP(BaseTestDiscovery): + fetcherClass = MockFetcherForXRIProxy + + documents = {'=smoker': ('application/xrds+xml', + readDataFile('yadis_2entries_idp.xml')) } + + def test_xri(self): + user_xri, services = discover.discoverXRI('=smoker') + self.failUnless(services, "Expected services, got zero") + self.failUnlessEqual(services[0].server_url, + "http://www.livejournal.com/openid/server.bml") + + +class TestPreferredNamespace(datadriven.DataDrivenTestCase): + def __init__(self, expected_ns, type_uris): + datadriven.DataDrivenTestCase.__init__( + self, 'Expecting %s from %s' % (expected_ns, type_uris)) + self.expected_ns = expected_ns + self.type_uris = type_uris + + def runOneTest(self): + endpoint = discover.OpenIDServiceEndpoint() + endpoint.type_uris = self.type_uris + actual_ns = endpoint.preferredNamespace() + self.failUnlessEqual(actual_ns, self.expected_ns) + + cases = [ + (message.OPENID1_NS, []), + (message.OPENID1_NS, ['http://jyte.com/']), + (message.OPENID1_NS, [discover.OPENID_1_0_TYPE]), + (message.OPENID1_NS, [discover.OPENID_1_1_TYPE]), + (message.OPENID2_NS, [discover.OPENID_2_0_TYPE]), + (message.OPENID2_NS, [discover.OPENID_IDP_2_0_TYPE]), + (message.OPENID2_NS, [discover.OPENID_2_0_TYPE, + discover.OPENID_1_0_TYPE]), + (message.OPENID2_NS, [discover.OPENID_1_0_TYPE, + discover.OPENID_2_0_TYPE]), + ] + +class TestIsOPIdentifier(unittest.TestCase): + def setUp(self): + self.endpoint = discover.OpenIDServiceEndpoint() + + def test_none(self): + self.failIf(self.endpoint.isOPIdentifier()) + + def test_openid1_0(self): + self.endpoint.type_uris = [discover.OPENID_1_0_TYPE] + self.failIf(self.endpoint.isOPIdentifier()) + + def test_openid1_1(self): + self.endpoint.type_uris = [discover.OPENID_1_1_TYPE] + self.failIf(self.endpoint.isOPIdentifier()) + + def test_openid2(self): + self.endpoint.type_uris = [discover.OPENID_2_0_TYPE] + self.failIf(self.endpoint.isOPIdentifier()) + + def test_openid2OP(self): + self.endpoint.type_uris = [discover.OPENID_IDP_2_0_TYPE] + self.failUnless(self.endpoint.isOPIdentifier()) + + def test_multipleMissing(self): + self.endpoint.type_uris = [discover.OPENID_2_0_TYPE, + discover.OPENID_1_0_TYPE] + self.failIf(self.endpoint.isOPIdentifier()) + + def test_multiplePresent(self): + self.endpoint.type_uris = [discover.OPENID_2_0_TYPE, + discover.OPENID_1_0_TYPE, + discover.OPENID_IDP_2_0_TYPE] + self.failUnless(self.endpoint.isOPIdentifier()) + +class TestFromOPEndpointURL(unittest.TestCase): + def setUp(self): + self.op_endpoint_url = 'http://example.com/op/endpoint' + self.endpoint = discover.OpenIDServiceEndpoint.fromOPEndpointURL( + self.op_endpoint_url) + + def test_isOPEndpoint(self): + self.failUnless(self.endpoint.isOPIdentifier()) + + def test_noIdentifiers(self): + self.failUnlessEqual(self.endpoint.getLocalID(), None) + self.failUnlessEqual(self.endpoint.claimed_id, None) + + def test_compatibility(self): + self.failIf(self.endpoint.compatibilityMode()) + + def test_canonicalID(self): + self.failUnlessEqual(self.endpoint.canonicalID, None) + + def test_serverURL(self): + self.failUnlessEqual(self.endpoint.server_url, self.op_endpoint_url) + +class TestDiscoverFunction(unittest.TestCase): + def setUp(self): + self._old_discoverURI = discover.discoverURI + self._old_discoverXRI = discover.discoverXRI + + discover.discoverXRI = self.discoverXRI + discover.discoverURI = self.discoverURI + + def tearDown(self): + discover.discoverURI = self._old_discoverURI + discover.discoverXRI = self._old_discoverXRI + + def discoverXRI(self, identifier): + return 'XRI' + + def discoverURI(self, identifier): + return 'URI' + + def test_uri(self): + self.failUnlessEqual('URI', discover.discover('http://woo!')) + + def test_uriForBogus(self): + self.failUnlessEqual('URI', discover.discover('not a URL or XRI')) + + def test_xri(self): + self.failUnlessEqual('XRI', discover.discover('xri://=something')) + + def test_xriChar(self): + self.failUnlessEqual('XRI', discover.discover('=something')) + +class TestEndpointSupportsType(unittest.TestCase): + def setUp(self): + self.endpoint = discover.OpenIDServiceEndpoint() + + def failUnlessSupportsOnly(self, *types): + for t in [ + 'foo', + discover.OPENID_1_1_TYPE, + discover.OPENID_1_0_TYPE, + discover.OPENID_2_0_TYPE, + discover.OPENID_IDP_2_0_TYPE, + ]: + if t in types: + self.failUnless(self.endpoint.supportsType(t), + "Must support %r" % (t,)) + else: + self.failIf(self.endpoint.supportsType(t), + "Shouldn't support %r" % (t,)) + + def test_supportsNothing(self): + self.failUnlessSupportsOnly() + + def test_openid2(self): + self.endpoint.type_uris = [discover.OPENID_2_0_TYPE] + self.failUnlessSupportsOnly(discover.OPENID_2_0_TYPE) + + def test_openid2provider(self): + self.endpoint.type_uris = [discover.OPENID_IDP_2_0_TYPE] + self.failUnlessSupportsOnly(discover.OPENID_IDP_2_0_TYPE, + discover.OPENID_2_0_TYPE) + + def test_openid1_0(self): + self.endpoint.type_uris = [discover.OPENID_1_0_TYPE] + self.failUnlessSupportsOnly(discover.OPENID_1_0_TYPE) + + def test_openid1_1(self): + self.endpoint.type_uris = [discover.OPENID_1_1_TYPE] + self.failUnlessSupportsOnly(discover.OPENID_1_1_TYPE) + + def test_multiple(self): + self.endpoint.type_uris = [discover.OPENID_1_1_TYPE, + discover.OPENID_2_0_TYPE] + self.failUnlessSupportsOnly(discover.OPENID_1_1_TYPE, + discover.OPENID_2_0_TYPE) + + def test_multipleWithProvider(self): + self.endpoint.type_uris = [discover.OPENID_1_1_TYPE, + discover.OPENID_2_0_TYPE, + discover.OPENID_IDP_2_0_TYPE] + self.failUnlessSupportsOnly(discover.OPENID_1_1_TYPE, + discover.OPENID_2_0_TYPE, + discover.OPENID_IDP_2_0_TYPE, + ) + + +class TestEndpointDisplayIdentifier(unittest.TestCase): + def test_strip_fragment(self): + endpoint = discover.OpenIDServiceEndpoint() + endpoint.claimed_id = 'http://recycled.invalid/#123' + self.failUnlessEqual('http://recycled.invalid/', endpoint.getDisplayIdentifier()) + + +def pyUnitTests(): + return datadriven.loadTests(__name__) + +if __name__ == '__main__': + suite = pyUnitTests() + runner = unittest.TextTestRunner() + runner.run(suite) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_etxrd.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_etxrd.py new file mode 100644 index 0000000..51cd27f --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_etxrd.py @@ -0,0 +1,194 @@ +import unittest +from openid.yadis import services, etxrd, xri +import os.path + +def datapath(filename): + module_directory = os.path.dirname(os.path.abspath(__file__)) + return os.path.join(module_directory, 'data', 'test_etxrd', filename) + +XRD_FILE = datapath('valid-populated-xrds.xml') +NOXRDS_FILE = datapath('not-xrds.xml') +NOXRD_FILE = datapath('no-xrd.xml') + +# None of the namespaces or service URIs below are official (or even +# sanctioned by the owners of that piece of URL-space) + +LID_2_0 = "http://lid.netmesh.org/sso/2.0b5" +TYPEKEY_1_0 = "http://typekey.com/services/1.0" + +def simpleOpenIDTransformer(endpoint): + """Function to extract information from an OpenID service element""" + if 'http://openid.net/signon/1.0' not in endpoint.type_uris: + return None + + delegates = list(endpoint.service_element.findall( + '{http://openid.net/xmlns/1.0}Delegate')) + assert len(delegates) == 1 + delegate = delegates[0].text + return (endpoint.uri, delegate) + +class TestServiceParser(unittest.TestCase): + def setUp(self): + self.xmldoc = file(XRD_FILE).read() + self.yadis_url = 'http://unittest.url/' + + def _getServices(self, flt=None): + return list(services.applyFilter(self.yadis_url, self.xmldoc, flt)) + + def testParse(self): + """Make sure that parsing succeeds at all""" + services = self._getServices() + + def testParseOpenID(self): + """Parse for OpenID services with a transformer function""" + services = self._getServices(simpleOpenIDTransformer) + + expectedServices = [ + ("http://www.myopenid.com/server", "http://josh.myopenid.com/"), + ("http://www.schtuff.com/openid", "http://users.schtuff.com/josh"), + ("http://www.livejournal.com/openid/server.bml", + "http://www.livejournal.com/users/nedthealpaca/"), + ] + + it = iter(services) + for (server_url, delegate) in expectedServices: + for (actual_url, actual_delegate) in it: + self.failUnlessEqual(server_url, actual_url) + self.failUnlessEqual(delegate, actual_delegate) + break + else: + self.fail('Not enough services found') + + def _checkServices(self, expectedServices): + """Check to make sure that the expected services are found in + that order in the parsed document.""" + it = iter(self._getServices()) + for (type_uri, uri) in expectedServices: + for service in it: + if type_uri in service.type_uris: + self.failUnlessEqual(service.uri, uri) + break + else: + self.fail('Did not find %r service' % (type_uri,)) + + def testGetSeveral(self): + """Get some services in order""" + expectedServices = [ + # type, URL + (TYPEKEY_1_0, None), + (LID_2_0, "http://mylid.net/josh"), + ] + + self._checkServices(expectedServices) + + def testGetSeveralForOne(self): + """Getting services for one Service with several Type elements.""" + types = [ 'http://lid.netmesh.org/sso/2.0b5' + , 'http://lid.netmesh.org/2.0b5' + ] + + uri = "http://mylid.net/josh" + + for service in self._getServices(): + if service.uri == uri: + found_types = service.matchTypes(types) + if found_types == types: + break + else: + self.fail('Did not find service with expected types and uris') + + def testNoXRDS(self): + """Make sure that we get an exception when an XRDS element is + not present""" + self.xmldoc = file(NOXRDS_FILE).read() + self.failUnlessRaises( + etxrd.XRDSError, + services.applyFilter, self.yadis_url, self.xmldoc, None) + + def testEmpty(self): + """Make sure that we get an exception when an XRDS element is + not present""" + self.xmldoc = '' + self.failUnlessRaises( + etxrd.XRDSError, + services.applyFilter, self.yadis_url, self.xmldoc, None) + + def testNoXRD(self): + """Make sure that we get an exception when there is no XRD + element present.""" + self.xmldoc = file(NOXRD_FILE).read() + self.failUnlessRaises( + etxrd.XRDSError, + services.applyFilter, self.yadis_url, self.xmldoc, None) + + +class TestCanonicalID(unittest.TestCase): + + def mkTest(iname, filename, expectedID): + """This function builds a method that runs the CanonicalID + test for the given set of inputs""" + + filename = datapath(filename) + def test(self): + xrds = etxrd.parseXRDS(file(filename).read()) + self._getCanonicalID(iname, xrds, expectedID) + return test + + test_delegated = mkTest( + "@ootao*test1", "delegated-20060809.xrds", + "@!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01") + + test_delegated_r1 = mkTest( + "@ootao*test1", "delegated-20060809-r1.xrds", + "@!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01") + + test_delegated_r2 = mkTest( + "@ootao*test1", "delegated-20060809-r2.xrds", + "@!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01") + + test_sometimesprefix = mkTest( + "@ootao*test1", "sometimesprefix.xrds", + "@!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01") + + test_prefixsometimes = mkTest( + "@ootao*test1", "prefixsometimes.xrds", + "@!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01") + + test_spoof1 = mkTest("=keturn*isDrummond", "spoof1.xrds", etxrd.XRDSFraud) + + test_spoof2 = mkTest("=keturn*isDrummond", "spoof2.xrds", etxrd.XRDSFraud) + + test_spoof3 = mkTest("@keturn*is*drummond", "spoof3.xrds", etxrd.XRDSFraud) + + test_status222 = mkTest("=x", "status222.xrds", None) + + test_multisegment_xri = mkTest('xri://=nishitani*masaki', + 'subsegments.xrds', + '=!E117.EF2F.454B.C707!0000.0000.3B9A.CA01') + + test_iri_auth_not_allowed = mkTest( + "phreak.example.com", "delegated-20060809-r2.xrds", etxrd.XRDSFraud) + test_iri_auth_not_allowed.__doc__ = \ + "Don't let IRI authorities be canonical for the GCS." + + # TODO: Refs + # test_ref = mkTest("@ootao*test.ref", "ref.xrds", "@!BAE.A650.823B.2475") + + # TODO: Add a IRI authority with an IRI canonicalID. + # TODO: Add test cases with real examples of multiple CanonicalIDs + # somewhere in the resolution chain. + + def _getCanonicalID(self, iname, xrds, expectedID): + if isinstance(expectedID, (str, unicode, type(None))): + cid = etxrd.getCanonicalID(iname, xrds) + self.failUnlessEqual(cid, expectedID and xri.XRI(expectedID)) + elif issubclass(expectedID, etxrd.XRDSError): + self.failUnlessRaises(expectedID, etxrd.getCanonicalID, + iname, xrds) + else: + self.fail("Don't know how to test for expected value %r" + % (expectedID,)) + + +if __name__ == '__main__': + unittest.main() diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_examples.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_examples.py new file mode 100644 index 0000000..92269d0 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_examples.py @@ -0,0 +1,185 @@ +"Test some examples." + +import socket +import os.path, unittest, sys, time +from cStringIO import StringIO + +import twill.commands, twill.parse, twill.unit + +from openid.consumer.discover import \ + OpenIDServiceEndpoint, OPENID_1_1_TYPE +from openid.consumer.consumer import AuthRequest + +class TwillTest(twill.unit.TestInfo): + """Variant of twill.unit.TestInfo that runs a function as a test script, + not twill script from a file. + """ + + # twill.unit is pretty small to start with, we're overriding + # run_script and bypassing twill.parse, so it may make sense to + # rewrite twill.unit altogether. + + # Desirable features: + # * better unittest.TestCase integration. + # - handle logs on setup and teardown. + # - treat TwillAssertionError as failed test assertion, make twill + # assertions more consistant with TestCase.failUnless idioms. + # - better error reporting on failed assertions. + # - The amount of functions passed back and forth between TestInfo + # and TestCase is currently pretty silly. + # * access to child process's logs. + # TestInfo.start_server redirects stdout/stderr to StringIO + # objects which are, afaict, inaccessible to the caller of + # test.unit.run_child_process. + # * notice when the child process dies, i.e. if you muck up and + # your runExampleServer function throws an exception. + + def run_script(self): + time.sleep(self.sleep) + # twill.commands.go(self.get_url()) + self.script(self) + + +def splitDir(d, count): + # in python2.4 and above, it's easier to spell this as + # d.rsplit(os.sep, count) + for i in xrange(count): + d = os.path.dirname(d) + return d + +def runExampleServer(host, port, data_path): + thisfile = os.path.abspath(sys.modules[__name__].__file__) + topDir = splitDir(thisfile, 3) + exampleDir = os.path.join(topDir, 'examples') + serverExample = os.path.join(exampleDir, 'server.py') + serverModule = {} + execfile(serverExample, serverModule) + serverMain = serverModule['main'] + + serverMain(host, port, data_path) + + + +class TestServer(unittest.TestCase): + """Acceptance tests for examples/server.py. + + These are more acceptance tests than unit tests as they actually + start the whole server running and test it on its external HTTP + interface. + """ + + def setUp(self): + self.twillOutput = StringIO() + self.twillErr = StringIO() + twill.set_output(self.twillOutput) + twill.set_errout(self.twillErr) + # FIXME: make sure we pick an available port. + self.server_port = 8080 + + # We need something to feed the server as a realm, but it needn't + # be reachable. (Until we test realm verification.) + self.realm = 'http://127.0.0.1/%s' % (self.id(),) + self.return_to = self.realm + '/return_to' + + twill.commands.reset_browser() + + + def runExampleServer(self): + """Zero-arg run-the-server function to be passed to TestInfo.""" + # FIXME - make sure sstore starts clean. + runExampleServer('127.0.0.1', self.server_port, 'sstore') + + + def v1endpoint(self, port): + """Return an OpenID 1.1 OpenIDServiceEndpoint for the server.""" + base = "http://%s:%s" % (socket.getfqdn('127.0.0.1'), port) + ep = OpenIDServiceEndpoint() + ep.claimed_id = base + "/id/bob" + ep.server_url = base + "/openidserver" + ep.type_uris = [OPENID_1_1_TYPE] + return ep + + + # TODO: test discovery + + def test_checkidv1(self): + """OpenID 1.1 checkid_setup request.""" + ti = TwillTest(self.twill_checkidv1, self.runExampleServer, + self.server_port, sleep=0.2) + twill.unit.run_test(ti) + + if self.twillErr.getvalue(): + self.fail(self.twillErr.getvalue()) + + + def test_allowed(self): + """OpenID 1.1 checkid_setup request.""" + ti = TwillTest(self.twill_allowed, self.runExampleServer, + self.server_port, sleep=0.2) + twill.unit.run_test(ti) + + if self.twillErr.getvalue(): + self.fail(self.twillErr.getvalue()) + + + def twill_checkidv1(self, twillInfo): + endpoint = self.v1endpoint(self.server_port) + authreq = AuthRequest(endpoint, assoc=None) + url = authreq.redirectURL(self.realm, self.return_to) + + c = twill.commands + + try: + c.go(url) + c.get_browser()._browser.set_handle_redirect(False) + c.submit("yes") + c.code(302) + headers = c.get_browser()._browser.response().info() + finalURL = headers['Location'] + self.failUnless('openid.mode=id_res' in finalURL, finalURL) + self.failUnless('openid.identity=' in finalURL, finalURL) + except twill.commands.TwillAssertionError, e: + msg = '%s\nFinal page:\n%s' % ( + str(e), c.get_browser().get_html()) + self.fail(msg) + + + def twill_allowed(self, twillInfo): + endpoint = self.v1endpoint(self.server_port) + authreq = AuthRequest(endpoint, assoc=None) + url = authreq.redirectURL(self.realm, self.return_to) + + c = twill.commands + + try: + c.go(url) + c.code(200) + c.get_browser()._browser.set_handle_redirect(False) + c.formvalue(1, 'remember', 'true') + c.find('name="login_as" value="bob"') + c.submit("yes") + c.code(302) + # Since we set remember=yes, the second time we shouldn't + # see that page. + c.go(url) + c.code(302) + headers = c.get_browser()._browser.response().info() + finalURL = headers['Location'] + self.failUnless(finalURL.startswith(self.return_to)) + except twill.commands.TwillAssertionError, e: + from traceback import format_exc + msg = '%s\nTwill output:%s\nTwill errors:%s\nFinal page:\n%s' % ( + format_exc(), + self.twillOutput.getvalue(), + self.twillErr.getvalue(), + c.get_browser().get_html()) + self.fail(msg) + + + def tearDown(self): + twill.set_output(None) + twill.set_errout(None) + + +if __name__ == '__main__': + unittest.main() diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_extension.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_extension.py new file mode 100644 index 0000000..7dadbd0 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_extension.py @@ -0,0 +1,36 @@ +from openid import extension +from openid import message + +import unittest + +class DummyExtension(extension.Extension): + ns_uri = 'http://an.extension/' + ns_alias = 'dummy' + + def getExtensionArgs(self): + return {} + +class ToMessageTest(unittest.TestCase): + def test_OpenID1(self): + oid1_msg = message.Message(message.OPENID1_NS) + ext = DummyExtension() + ext.toMessage(oid1_msg) + namespaces = oid1_msg.namespaces + self.failUnless(namespaces.isImplicit(DummyExtension.ns_uri)) + self.failUnlessEqual( + DummyExtension.ns_uri, + namespaces.getNamespaceURI(DummyExtension.ns_alias)) + self.failUnlessEqual(DummyExtension.ns_alias, + namespaces.getAlias(DummyExtension.ns_uri)) + + def test_OpenID2(self): + oid2_msg = message.Message(message.OPENID2_NS) + ext = DummyExtension() + ext.toMessage(oid2_msg) + namespaces = oid2_msg.namespaces + self.failIf(namespaces.isImplicit(DummyExtension.ns_uri)) + self.failUnlessEqual( + DummyExtension.ns_uri, + namespaces.getNamespaceURI(DummyExtension.ns_alias)) + self.failUnlessEqual(DummyExtension.ns_alias, + namespaces.getAlias(DummyExtension.ns_uri)) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_fetchers.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_fetchers.py new file mode 100644 index 0000000..da1eea8 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_fetchers.py @@ -0,0 +1,285 @@ +import warnings +import unittest +import sys +import urllib2 +import socket + +from openid import fetchers + +# XXX: make these separate test cases + +def failUnlessResponseExpected(expected, actual): + assert expected.final_url == actual.final_url, ( + "%r != %r" % (expected.final_url, actual.final_url)) + assert expected.status == actual.status + assert expected.body == actual.body + got_headers = dict(actual.headers) + del got_headers['date'] + del got_headers['server'] + for k, v in expected.headers.iteritems(): + assert got_headers[k] == v, (k, v, got_headers[k]) + +def test_fetcher(fetcher, exc, server): + def geturl(path): + return 'http://%s:%s%s' % (socket.getfqdn(server.server_name), + server.socket.getsockname()[1], + path) + + expected_headers = {'content-type':'text/plain'} + + def plain(path, code): + path = '/' + path + expected = fetchers.HTTPResponse( + geturl(path), code, expected_headers, path) + return (path, expected) + + expect_success = fetchers.HTTPResponse( + geturl('/success'), 200, expected_headers, '/success') + cases = [ + ('/success', expect_success), + ('/301redirect', expect_success), + ('/302redirect', expect_success), + ('/303redirect', expect_success), + ('/307redirect', expect_success), + plain('notfound', 404), + plain('badreq', 400), + plain('forbidden', 403), + plain('error', 500), + plain('server_error', 503), + ] + + for path, expected in cases: + fetch_url = geturl(path) + try: + actual = fetcher.fetch(fetch_url) + except (SystemExit, KeyboardInterrupt): + pass + except: + print fetcher, fetch_url + raise + else: + failUnlessResponseExpected(expected, actual) + + for err_url in [geturl('/closed'), + 'http://invalid.janrain.com/', + 'not:a/url', + 'ftp://janrain.com/pub/']: + try: + result = fetcher.fetch(err_url) + except (KeyboardInterrupt, SystemExit): + raise + except fetchers.HTTPError, why: + # This is raised by the Curl fetcher for bad cases + # detected by the fetchers module, but it's a subclass of + # HTTPFetchingError, so we have to catch it explicitly. + assert exc + except fetchers.HTTPFetchingError, why: + assert not exc, (fetcher, exc, server) + except: + assert exc + else: + assert False, 'An exception was expected for %r (%r)' % (fetcher, result) + +def run_fetcher_tests(server): + exc_fetchers = [] + for klass, library_name in [ + (fetchers.Urllib2Fetcher, 'urllib2'), + (fetchers.CurlHTTPFetcher, 'pycurl'), + (fetchers.HTTPLib2Fetcher, 'httplib2'), + ]: + try: + exc_fetchers.append(klass()) + except RuntimeError, why: + if why[0].startswith('Cannot find %s library' % (library_name,)): + try: + __import__(library_name) + except ImportError: + warnings.warn( + 'Skipping tests for %r fetcher because ' + 'the library did not import.' % (library_name,)) + pass + else: + assert False, ('%s present but not detected' % (library_name,)) + else: + raise + + non_exc_fetchers = [] + for f in exc_fetchers: + non_exc_fetchers.append(fetchers.ExceptionWrappingFetcher(f)) + + for f in exc_fetchers: + test_fetcher(f, True, server) + + for f in non_exc_fetchers: + test_fetcher(f, False, server) + +from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer + +class FetcherTestHandler(BaseHTTPRequestHandler): + cases = { + '/success':(200, None), + '/301redirect':(301, '/success'), + '/302redirect':(302, '/success'), + '/303redirect':(303, '/success'), + '/307redirect':(307, '/success'), + '/notfound':(404, None), + '/badreq':(400, None), + '/forbidden':(403, None), + '/error':(500, None), + '/server_error':(503, None), + } + + def log_request(self, *args): + pass + + def do_GET(self): + if self.path == '/closed': + self.wfile.close() + else: + try: + http_code, location = self.cases[self.path] + except KeyError: + self.errorResponse('Bad path') + else: + extra_headers = [('Content-type', 'text/plain')] + if location is not None: + host, port = self.server.server_address + base = ('http://%s:%s' % (socket.getfqdn(host), port,)) + location = base + location + extra_headers.append(('Location', location)) + self._respond(http_code, extra_headers, self.path) + + def do_POST(self): + try: + http_code, extra_headers = self.cases[self.path] + except KeyError: + self.errorResponse('Bad path') + else: + if http_code in [301, 302, 303, 307]: + self.errorResponse() + else: + content_type = self.headers.get('content-type', 'text/plain') + extra_headers.append(('Content-type', content_type)) + content_length = int(self.headers.get('Content-length', '-1')) + body = self.rfile.read(content_length) + self._respond(http_code, extra_headers, body) + + def errorResponse(self, message=None): + req = [ + ('HTTP method', self.command), + ('path', self.path), + ] + if message: + req.append(('message', message)) + + body_parts = ['Bad request:\r\n'] + for k, v in req: + body_parts.append(' %s: %s\r\n' % (k, v)) + body = ''.join(body_parts) + self._respond(400, [('Content-type', 'text/plain')], body) + + def _respond(self, http_code, extra_headers, body): + self.send_response(http_code) + for k, v in extra_headers: + self.send_header(k, v) + self.end_headers() + self.wfile.write(body) + self.wfile.close() + + def finish(self): + if not self.wfile.closed: + self.wfile.flush() + self.wfile.close() + self.rfile.close() + +def test(): + import socket + host = socket.getfqdn('127.0.0.1') + # When I use port 0 here, it works for the first fetch and the + # next one gets connection refused. Bummer. So instead, pick a + # port that's *probably* not in use. + import os + port = (os.getpid() % 31000) + 1024 + + server = HTTPServer((host, port), FetcherTestHandler) + + import threading + server_thread = threading.Thread(target=server.serve_forever) + server_thread.setDaemon(True) + server_thread.start() + + run_fetcher_tests(server) + +class FakeFetcher(object): + sentinel = object() + + def fetch(self, *args, **kwargs): + return self.sentinel + +class DefaultFetcherTest(unittest.TestCase): + def setUp(self): + """reset the default fetcher to None""" + fetchers.setDefaultFetcher(None) + + def tearDown(self): + """reset the default fetcher to None""" + fetchers.setDefaultFetcher(None) + + def test_getDefaultNotNone(self): + """Make sure that None is never returned as a default fetcher""" + self.failUnless(fetchers.getDefaultFetcher() is not None) + fetchers.setDefaultFetcher(None) + self.failUnless(fetchers.getDefaultFetcher() is not None) + + def test_setDefault(self): + """Make sure the getDefaultFetcher returns the object set for + setDefaultFetcher""" + sentinel = object() + fetchers.setDefaultFetcher(sentinel, wrap_exceptions=False) + self.failUnless(fetchers.getDefaultFetcher() is sentinel) + + def test_callFetch(self): + """Make sure that fetchers.fetch() uses the default fetcher + instance that was set.""" + fetchers.setDefaultFetcher(FakeFetcher()) + actual = fetchers.fetch('bad://url') + self.failUnless(actual is FakeFetcher.sentinel) + + def test_wrappedByDefault(self): + """Make sure that the default fetcher instance wraps + exceptions by default""" + default_fetcher = fetchers.getDefaultFetcher() + self.failUnless(isinstance(default_fetcher, + fetchers.ExceptionWrappingFetcher), + default_fetcher) + + self.failUnlessRaises(fetchers.HTTPFetchingError, + fetchers.fetch, 'http://invalid.janrain.com/') + + def test_notWrapped(self): + """Make sure that if we set a non-wrapped fetcher as default, + it will not wrap exceptions.""" + # A fetcher that will raise an exception when it encounters a + # host that will not resolve + fetcher = fetchers.Urllib2Fetcher() + fetchers.setDefaultFetcher(fetcher, wrap_exceptions=False) + + self.failIf(isinstance(fetchers.getDefaultFetcher(), + fetchers.ExceptionWrappingFetcher)) + + try: + fetchers.fetch('http://invalid.janrain.com/') + except fetchers.HTTPFetchingError: + self.fail('Should not be wrapping exception') + except: + exc = sys.exc_info()[1] + self.failUnless(isinstance(exc, urllib2.URLError), exc) + pass + else: + self.fail('Should have raised an exception') + +def pyUnitTests(): + case1 = unittest.FunctionTestCase(test) + loadTests = unittest.defaultTestLoader.loadTestsFromTestCase + case2 = loadTests(DefaultFetcherTest) + return unittest.TestSuite([case1, case2]) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_htmldiscover.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_htmldiscover.py new file mode 100644 index 0000000..0a49e16 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_htmldiscover.py @@ -0,0 +1,21 @@ +from openid.consumer.discover import OpenIDServiceEndpoint +import datadriven + +class BadLinksTestCase(datadriven.DataDrivenTestCase): + cases = [ + '', + "http://not.in.a.link.tag/", + '', + ] + + def __init__(self, data): + datadriven.DataDrivenTestCase.__init__(self, data) + self.data = data + + def runOneTest(self): + actual = OpenIDServiceEndpoint.fromHTML('http://unused.url/', self.data) + expected = [] + self.failUnlessEqual(expected, actual) + +def pyUnitTests(): + return datadriven.loadTests(__name__) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_message.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_message.py new file mode 100644 index 0000000..0bbd280 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_message.py @@ -0,0 +1,998 @@ +from openid import message +from openid import oidutil +from openid.extensions import sreg + +import urllib +import cgi +import unittest + +def mkGetArgTest(ns, key, expected=None): + def test(self): + a_default = object() + self.failUnlessEqual(self.msg.getArg(ns, key), expected) + if expected is None: + self.failUnlessEqual( + self.msg.getArg(ns, key, a_default), a_default) + self.failUnlessRaises( + KeyError, self.msg.getArg, ns, key, message.no_default) + else: + self.failUnlessEqual( + self.msg.getArg(ns, key, a_default), expected) + self.failUnlessEqual( + self.msg.getArg(ns, key, message.no_default), expected) + + return test + +class EmptyMessageTest(unittest.TestCase): + def setUp(self): + self.msg = message.Message() + + def test_toPostArgs(self): + self.failUnlessEqual(self.msg.toPostArgs(), {}) + + def test_toArgs(self): + self.failUnlessEqual(self.msg.toArgs(), {}) + + def test_toKVForm(self): + self.failUnlessEqual(self.msg.toKVForm(), '') + + def test_toURLEncoded(self): + self.failUnlessEqual(self.msg.toURLEncoded(), '') + + def test_toURL(self): + base_url = 'http://base.url/' + self.failUnlessEqual(self.msg.toURL(base_url), base_url) + + def test_getOpenID(self): + self.failUnlessEqual(self.msg.getOpenIDNamespace(), None) + + def test_getKeyOpenID(self): + # Could reasonably return None instead of raising an + # exception. I'm not sure which one is more right, since this + # case should only happen when you're building a message from + # scratch and so have no default namespace. + self.failUnlessRaises(message.UndefinedOpenIDNamespace, + self.msg.getKey, message.OPENID_NS, 'foo') + + def test_getKeyBARE(self): + self.failUnlessEqual(self.msg.getKey(message.BARE_NS, 'foo'), 'foo') + + def test_getKeyNS1(self): + self.failUnlessEqual(self.msg.getKey(message.OPENID1_NS, 'foo'), None) + + def test_getKeyNS2(self): + self.failUnlessEqual(self.msg.getKey(message.OPENID2_NS, 'foo'), None) + + def test_getKeyNS3(self): + self.failUnlessEqual(self.msg.getKey('urn:nothing-significant', 'foo'), + None) + + def test_hasKey(self): + # Could reasonably return False instead of raising an + # exception. I'm not sure which one is more right, since this + # case should only happen when you're building a message from + # scratch and so have no default namespace. + self.failUnlessRaises(message.UndefinedOpenIDNamespace, + self.msg.hasKey, message.OPENID_NS, 'foo') + + def test_hasKeyBARE(self): + self.failUnlessEqual(self.msg.hasKey(message.BARE_NS, 'foo'), False) + + def test_hasKeyNS1(self): + self.failUnlessEqual(self.msg.hasKey(message.OPENID1_NS, 'foo'), False) + + def test_hasKeyNS2(self): + self.failUnlessEqual(self.msg.hasKey(message.OPENID2_NS, 'foo'), False) + + def test_hasKeyNS3(self): + self.failUnlessEqual(self.msg.hasKey('urn:nothing-significant', 'foo'), + False) + + def test_getAliasedArgSuccess(self): + msg = message.Message.fromPostArgs({'openid.ns.test': 'urn://foo', + 'openid.test.flub': 'bogus'}) + actual_uri = msg.getAliasedArg('ns.test', message.no_default) + self.assertEquals("urn://foo", actual_uri) + + def test_getAliasedArgFailure(self): + msg = message.Message.fromPostArgs({'openid.test.flub': 'bogus'}) + self.assertRaises(KeyError, + msg.getAliasedArg, 'ns.test', message.no_default) + + def test_getArg(self): + # Could reasonably return None instead of raising an + # exception. I'm not sure which one is more right, since this + # case should only happen when you're building a message from + # scratch and so have no default namespace. + self.failUnlessRaises(message.UndefinedOpenIDNamespace, + self.msg.getArg, message.OPENID_NS, 'foo') + + test_getArgBARE = mkGetArgTest(message.BARE_NS, 'foo') + test_getArgNS1 = mkGetArgTest(message.OPENID1_NS, 'foo') + test_getArgNS2 = mkGetArgTest(message.OPENID2_NS, 'foo') + test_getArgNS3 = mkGetArgTest('urn:nothing-significant', 'foo') + + def test_getArgs(self): + # Could reasonably return {} instead of raising an + # exception. I'm not sure which one is more right, since this + # case should only happen when you're building a message from + # scratch and so have no default namespace. + self.failUnlessRaises(message.UndefinedOpenIDNamespace, + self.msg.getArgs, message.OPENID_NS) + + def test_getArgsBARE(self): + self.failUnlessEqual(self.msg.getArgs(message.BARE_NS), {}) + + def test_getArgsNS1(self): + self.failUnlessEqual(self.msg.getArgs(message.OPENID1_NS), {}) + + def test_getArgsNS2(self): + self.failUnlessEqual(self.msg.getArgs(message.OPENID2_NS), {}) + + def test_getArgsNS3(self): + self.failUnlessEqual(self.msg.getArgs('urn:nothing-significant'), {}) + + def test_updateArgs(self): + self.failUnlessRaises(message.UndefinedOpenIDNamespace, + self.msg.updateArgs, message.OPENID_NS, + {'does not':'matter'}) + + def _test_updateArgsNS(self, ns): + update_args = { + 'Camper van Beethoven':'David Lowery', + 'Magnolia Electric Co.':'Jason Molina', + } + + self.failUnlessEqual(self.msg.getArgs(ns), {}) + self.msg.updateArgs(ns, update_args) + self.failUnlessEqual(self.msg.getArgs(ns), update_args) + + def test_updateArgsBARE(self): + self._test_updateArgsNS(message.BARE_NS) + + def test_updateArgsNS1(self): + self._test_updateArgsNS(message.OPENID1_NS) + + def test_updateArgsNS2(self): + self._test_updateArgsNS(message.OPENID2_NS) + + def test_updateArgsNS3(self): + self._test_updateArgsNS('urn:nothing-significant') + + def test_setArg(self): + self.failUnlessRaises(message.UndefinedOpenIDNamespace, + self.msg.setArg, message.OPENID_NS, + 'does not', 'matter') + + def _test_setArgNS(self, ns): + key = 'Camper van Beethoven' + value = 'David Lowery' + self.failUnlessEqual(self.msg.getArg(ns, key), None) + self.msg.setArg(ns, key, value) + self.failUnlessEqual(self.msg.getArg(ns, key), value) + + def test_setArgBARE(self): + self._test_setArgNS(message.BARE_NS) + + def test_setArgNS1(self): + self._test_setArgNS(message.OPENID1_NS) + + def test_setArgNS2(self): + self._test_setArgNS(message.OPENID2_NS) + + def test_setArgNS3(self): + self._test_setArgNS('urn:nothing-significant') + + def test_setArgToNone(self): + self.failUnlessRaises(AssertionError, self.msg.setArg, + message.OPENID1_NS, 'op_endpoint', None) + + def test_delArg(self): + # Could reasonably raise KeyError instead of raising + # UndefinedOpenIDNamespace. I'm not sure which one is more + # right, since this case should only happen when you're + # building a message from scratch and so have no default + # namespace. + self.failUnlessRaises(message.UndefinedOpenIDNamespace, + self.msg.delArg, message.OPENID_NS, 'key') + + def _test_delArgNS(self, ns): + key = 'Camper van Beethoven' + self.failUnlessRaises(KeyError, self.msg.delArg, ns, key) + + def test_delArgBARE(self): + self._test_delArgNS(message.BARE_NS) + + def test_delArgNS1(self): + self._test_delArgNS(message.OPENID1_NS) + + def test_delArgNS2(self): + self._test_delArgNS(message.OPENID2_NS) + + def test_delArgNS3(self): + self._test_delArgNS('urn:nothing-significant') + + def test_isOpenID1(self): + self.failIf(self.msg.isOpenID1()) + + def test_isOpenID2(self): + self.failIf(self.msg.isOpenID2()) + +class OpenID1MessageTest(unittest.TestCase): + def setUp(self): + self.msg = message.Message.fromPostArgs({'openid.mode':'error', + 'openid.error':'unit test'}) + + def test_toPostArgs(self): + self.failUnlessEqual(self.msg.toPostArgs(), + {'openid.mode':'error', + 'openid.error':'unit test'}) + + def test_toArgs(self): + self.failUnlessEqual(self.msg.toArgs(), {'mode':'error', + 'error':'unit test'}) + + def test_toKVForm(self): + self.failUnlessEqual(self.msg.toKVForm(), + 'error:unit test\nmode:error\n') + + def test_toURLEncoded(self): + self.failUnlessEqual(self.msg.toURLEncoded(), + 'openid.error=unit+test&openid.mode=error') + + def test_toURL(self): + base_url = 'http://base.url/' + actual = self.msg.toURL(base_url) + actual_base = actual[:len(base_url)] + self.failUnlessEqual(actual_base, base_url) + self.failUnlessEqual(actual[len(base_url)], '?') + query = actual[len(base_url) + 1:] + parsed = cgi.parse_qs(query) + self.failUnlessEqual(parsed, {'openid.mode':['error'], + 'openid.error':['unit test']}) + + def test_getOpenID(self): + self.failUnlessEqual(self.msg.getOpenIDNamespace(), message.OPENID1_NS) + + def test_getKeyOpenID(self): + self.failUnlessEqual(self.msg.getKey(message.OPENID_NS, 'mode'), + 'openid.mode') + + def test_getKeyBARE(self): + self.failUnlessEqual(self.msg.getKey(message.BARE_NS, 'mode'), 'mode') + + def test_getKeyNS1(self): + self.failUnlessEqual( + self.msg.getKey(message.OPENID1_NS, 'mode'), 'openid.mode') + + def test_getKeyNS2(self): + self.failUnlessEqual(self.msg.getKey(message.OPENID2_NS, 'mode'), None) + + def test_getKeyNS3(self): + self.failUnlessEqual( + self.msg.getKey('urn:nothing-significant', 'mode'), None) + + def test_hasKey(self): + self.failUnlessEqual(self.msg.hasKey(message.OPENID_NS, 'mode'), True) + + def test_hasKeyBARE(self): + self.failUnlessEqual(self.msg.hasKey(message.BARE_NS, 'mode'), False) + + def test_hasKeyNS1(self): + self.failUnlessEqual(self.msg.hasKey(message.OPENID1_NS, 'mode'), True) + + def test_hasKeyNS2(self): + self.failUnlessEqual( + self.msg.hasKey(message.OPENID2_NS, 'mode'), False) + + def test_hasKeyNS3(self): + self.failUnlessEqual( + self.msg.hasKey('urn:nothing-significant', 'mode'), False) + + test_getArgBARE = mkGetArgTest(message.BARE_NS, 'mode') + test_getArgNS = mkGetArgTest(message.OPENID_NS, 'mode', 'error') + test_getArgNS1 = mkGetArgTest(message.OPENID1_NS, 'mode', 'error') + test_getArgNS2 = mkGetArgTest(message.OPENID2_NS, 'mode') + test_getArgNS3 = mkGetArgTest('urn:nothing-significant', 'mode') + + def test_getArgs(self): + self.failUnlessEqual(self.msg.getArgs(message.OPENID_NS), + {'mode':'error', + 'error':'unit test', + }) + + def test_getArgsBARE(self): + self.failUnlessEqual(self.msg.getArgs(message.BARE_NS), {}) + + def test_getArgsNS1(self): + self.failUnlessEqual(self.msg.getArgs(message.OPENID1_NS), + {'mode':'error', + 'error':'unit test', + }) + + def test_getArgsNS2(self): + self.failUnlessEqual(self.msg.getArgs(message.OPENID2_NS), {}) + + def test_getArgsNS3(self): + self.failUnlessEqual(self.msg.getArgs('urn:nothing-significant'), {}) + + def _test_updateArgsNS(self, ns, before=None): + if before is None: + before = {} + update_args = { + 'Camper van Beethoven':'David Lowery', + 'Magnolia Electric Co.':'Jason Molina', + } + + self.failUnlessEqual(self.msg.getArgs(ns), before) + self.msg.updateArgs(ns, update_args) + after = dict(before) + after.update(update_args) + self.failUnlessEqual(self.msg.getArgs(ns), after) + + def test_updateArgs(self): + self._test_updateArgsNS(message.OPENID_NS, + before={'mode':'error', 'error':'unit test'}) + + def test_updateArgsBARE(self): + self._test_updateArgsNS(message.BARE_NS) + + def test_updateArgsNS1(self): + self._test_updateArgsNS(message.OPENID1_NS, + before={'mode':'error', 'error':'unit test'}) + + def test_updateArgsNS2(self): + self._test_updateArgsNS(message.OPENID2_NS) + + def test_updateArgsNS3(self): + self._test_updateArgsNS('urn:nothing-significant') + + def _test_setArgNS(self, ns): + key = 'Camper van Beethoven' + value = 'David Lowery' + self.failUnlessEqual(self.msg.getArg(ns, key), None) + self.msg.setArg(ns, key, value) + self.failUnlessEqual(self.msg.getArg(ns, key), value) + + def test_setArg(self): + self._test_setArgNS(message.OPENID_NS) + + def test_setArgBARE(self): + self._test_setArgNS(message.BARE_NS) + + def test_setArgNS1(self): + self._test_setArgNS(message.OPENID1_NS) + + def test_setArgNS2(self): + self._test_setArgNS(message.OPENID2_NS) + + def test_setArgNS3(self): + self._test_setArgNS('urn:nothing-significant') + + def _test_delArgNS(self, ns): + key = 'Camper van Beethoven' + value = 'David Lowery' + + self.failUnlessRaises(KeyError, self.msg.delArg, ns, key) + self.msg.setArg(ns, key, value) + self.failUnlessEqual(self.msg.getArg(ns, key), value) + self.msg.delArg(ns, key) + self.failUnlessEqual(self.msg.getArg(ns, key), None) + + def test_delArg(self): + self._test_delArgNS(message.OPENID_NS) + + def test_delArgBARE(self): + self._test_delArgNS(message.BARE_NS) + + def test_delArgNS1(self): + self._test_delArgNS(message.OPENID1_NS) + + def test_delArgNS2(self): + self._test_delArgNS(message.OPENID2_NS) + + def test_delArgNS3(self): + self._test_delArgNS('urn:nothing-significant') + + + def test_isOpenID1(self): + self.failUnless(self.msg.isOpenID1()) + + def test_isOpenID2(self): + self.failIf(self.msg.isOpenID2()) + +class OpenID1ExplicitMessageTest(unittest.TestCase): + def setUp(self): + self.msg = message.Message.fromPostArgs({'openid.mode':'error', + 'openid.error':'unit test', + 'openid.ns':message.OPENID1_NS + }) + + def test_toPostArgs(self): + self.failUnlessEqual(self.msg.toPostArgs(), + {'openid.mode':'error', + 'openid.error':'unit test', + 'openid.ns':message.OPENID1_NS + }) + + def test_toArgs(self): + self.failUnlessEqual(self.msg.toArgs(), {'mode':'error', + 'error':'unit test', + 'ns':message.OPENID1_NS}) + + def test_toKVForm(self): + self.failUnlessEqual(self.msg.toKVForm(), + 'error:unit test\nmode:error\nns:%s\n' + %message.OPENID1_NS) + + def test_toURLEncoded(self): + self.failUnlessEqual(self.msg.toURLEncoded(), + 'openid.error=unit+test&openid.mode=error&openid.ns=http%3A%2F%2Fopenid.net%2Fsignon%2F1.0') + + def test_toURL(self): + base_url = 'http://base.url/' + actual = self.msg.toURL(base_url) + actual_base = actual[:len(base_url)] + self.failUnlessEqual(actual_base, base_url) + self.failUnlessEqual(actual[len(base_url)], '?') + query = actual[len(base_url) + 1:] + parsed = cgi.parse_qs(query) + self.failUnlessEqual(parsed, {'openid.mode':['error'], + 'openid.error':['unit test'], + 'openid.ns':[message.OPENID1_NS] + }) + + def test_isOpenID1(self): + self.failUnless(self.msg.isOpenID1()) + + +class OpenID2MessageTest(unittest.TestCase): + def setUp(self): + self.msg = message.Message.fromPostArgs({'openid.mode':'error', + 'openid.error':'unit test', + 'openid.ns':message.OPENID2_NS + }) + self.msg.setArg(message.BARE_NS, "xey", "value") + + def test_toPostArgs(self): + self.failUnlessEqual(self.msg.toPostArgs(), + {'openid.mode':'error', + 'openid.error':'unit test', + 'openid.ns':message.OPENID2_NS, + 'xey': 'value', + }) + + def test_toArgs(self): + # This method can't tolerate BARE_NS. + self.msg.delArg(message.BARE_NS, "xey") + self.failUnlessEqual(self.msg.toArgs(), {'mode':'error', + 'error':'unit test', + 'ns':message.OPENID2_NS, + }) + + def test_toKVForm(self): + # Can't tolerate BARE_NS in kvform + self.msg.delArg(message.BARE_NS, "xey") + self.failUnlessEqual(self.msg.toKVForm(), + 'error:unit test\nmode:error\nns:%s\n' % + (message.OPENID2_NS,)) + + def _test_urlencoded(self, s): + expected = ('openid.error=unit+test&openid.mode=error&' + 'openid.ns=%s&xey=value' % ( + urllib.quote(message.OPENID2_NS, ''),)) + self.failUnlessEqual(s, expected) + + + def test_toURLEncoded(self): + self._test_urlencoded(self.msg.toURLEncoded()) + + def test_toURL(self): + base_url = 'http://base.url/' + actual = self.msg.toURL(base_url) + actual_base = actual[:len(base_url)] + self.failUnlessEqual(actual_base, base_url) + self.failUnlessEqual(actual[len(base_url)], '?') + query = actual[len(base_url) + 1:] + self._test_urlencoded(query) + + def test_getOpenID(self): + self.failUnlessEqual(self.msg.getOpenIDNamespace(), message.OPENID2_NS) + + def test_getKeyOpenID(self): + self.failUnlessEqual(self.msg.getKey(message.OPENID_NS, 'mode'), + 'openid.mode') + + def test_getKeyBARE(self): + self.failUnlessEqual(self.msg.getKey(message.BARE_NS, 'mode'), 'mode') + + def test_getKeyNS1(self): + self.failUnlessEqual( + self.msg.getKey(message.OPENID1_NS, 'mode'), None) + + def test_getKeyNS2(self): + self.failUnlessEqual( + self.msg.getKey(message.OPENID2_NS, 'mode'), 'openid.mode') + + def test_getKeyNS3(self): + self.failUnlessEqual( + self.msg.getKey('urn:nothing-significant', 'mode'), None) + + def test_hasKeyOpenID(self): + self.failUnlessEqual(self.msg.hasKey(message.OPENID_NS, 'mode'), True) + + def test_hasKeyBARE(self): + self.failUnlessEqual(self.msg.hasKey(message.BARE_NS, 'mode'), False) + + def test_hasKeyNS1(self): + self.failUnlessEqual( + self.msg.hasKey(message.OPENID1_NS, 'mode'), False) + + def test_hasKeyNS2(self): + self.failUnlessEqual( + self.msg.hasKey(message.OPENID2_NS, 'mode'), True) + + def test_hasKeyNS3(self): + self.failUnlessEqual( + self.msg.hasKey('urn:nothing-significant', 'mode'), False) + + test_getArgBARE = mkGetArgTest(message.BARE_NS, 'mode') + test_getArgNS = mkGetArgTest(message.OPENID_NS, 'mode', 'error') + test_getArgNS1 = mkGetArgTest(message.OPENID1_NS, 'mode') + test_getArgNS2 = mkGetArgTest(message.OPENID2_NS, 'mode', 'error') + test_getArgNS3 = mkGetArgTest('urn:nothing-significant', 'mode') + + def test_getArgsOpenID(self): + self.failUnlessEqual(self.msg.getArgs(message.OPENID_NS), + {'mode':'error', + 'error':'unit test', + }) + + def test_getArgsBARE(self): + self.failUnlessEqual(self.msg.getArgs(message.BARE_NS), + {'xey': 'value'}) + + def test_getArgsNS1(self): + self.failUnlessEqual(self.msg.getArgs(message.OPENID1_NS), {}) + + def test_getArgsNS2(self): + self.failUnlessEqual(self.msg.getArgs(message.OPENID2_NS), + {'mode':'error', + 'error':'unit test', + }) + + def test_getArgsNS3(self): + self.failUnlessEqual(self.msg.getArgs('urn:nothing-significant'), {}) + + def _test_updateArgsNS(self, ns, before=None): + if before is None: + before = {} + update_args = { + 'Camper van Beethoven':'David Lowery', + 'Magnolia Electric Co.':'Jason Molina', + } + + self.failUnlessEqual(self.msg.getArgs(ns), before) + self.msg.updateArgs(ns, update_args) + after = dict(before) + after.update(update_args) + self.failUnlessEqual(self.msg.getArgs(ns), after) + + def test_updateArgsOpenID(self): + self._test_updateArgsNS(message.OPENID_NS, + before={'mode':'error', 'error':'unit test'}) + + def test_updateArgsBARE(self): + self._test_updateArgsNS(message.BARE_NS, + before={'xey':'value'}) + + def test_updateArgsNS1(self): + self._test_updateArgsNS(message.OPENID1_NS) + + def test_updateArgsNS2(self): + self._test_updateArgsNS(message.OPENID2_NS, + before={'mode':'error', 'error':'unit test'}) + + def test_updateArgsNS3(self): + self._test_updateArgsNS('urn:nothing-significant') + + def _test_setArgNS(self, ns): + key = 'Camper van Beethoven' + value = 'David Lowery' + self.failUnlessEqual(self.msg.getArg(ns, key), None) + self.msg.setArg(ns, key, value) + self.failUnlessEqual(self.msg.getArg(ns, key), value) + + def test_setArgOpenID(self): + self._test_setArgNS(message.OPENID_NS) + + def test_setArgBARE(self): + self._test_setArgNS(message.BARE_NS) + + def test_setArgNS1(self): + self._test_setArgNS(message.OPENID1_NS) + + def test_setArgNS2(self): + self._test_setArgNS(message.OPENID2_NS) + + def test_setArgNS3(self): + self._test_setArgNS('urn:nothing-significant') + + def test_badAlias(self): + """Make sure dotted aliases and OpenID protocol fields are not + allowed as namespace aliases.""" + + for f in message.OPENID_PROTOCOL_FIELDS + ['dotted.alias']: + args = {'openid.ns.%s' % f: 'blah', + 'openid.%s.foo' % f: 'test'} + + # .fromPostArgs covers .fromPostArgs, .fromOpenIDArgs, + # ._fromOpenIDArgs, and .fromOpenIDArgs (since it calls + # .fromPostArgs). + self.failUnlessRaises(AssertionError, self.msg.fromPostArgs, + args) + + def test_mysterious_missing_namespace_bug(self): + """A failing test for bug #112""" + openid_args = { + 'assoc_handle': '{{HMAC-SHA256}{1211477242.29743}{v5cadg==}', + 'claimed_id': 'http://nerdbank.org/OPAffirmative/AffirmativeIdentityWithSregNoAssoc.aspx', + 'ns.sreg': 'http://openid.net/extensions/sreg/1.1', + 'response_nonce': '2008-05-22T17:27:22ZUoW5.\\NV', + 'signed': 'return_to,identity,claimed_id,op_endpoint,response_nonce,ns.sreg,sreg.email,sreg.nickname,assoc_handle', + 'sig': 'e3eGZ10+TNRZitgq5kQlk5KmTKzFaCRI8OrRoXyoFa4=', + 'mode': 'check_authentication', + 'op_endpoint': 'http://nerdbank.org/OPAffirmative/ProviderNoAssoc.aspx', + 'sreg.nickname': 'Andy', + 'return_to': 'http://localhost.localdomain:8001/process?janrain_nonce=2008-05-22T17%3A27%3A21ZnxHULd', + 'invalidate_handle': '{{HMAC-SHA1}{1211477241.92242}{H0akXw==}', + 'identity': 'http://nerdbank.org/OPAffirmative/AffirmativeIdentityWithSregNoAssoc.aspx', + 'sreg.email': 'a@b.com' + } + m = message.Message.fromOpenIDArgs(openid_args) + + self.failUnless(('http://openid.net/extensions/sreg/1.1', 'sreg') in + list(m.namespaces.iteritems())) + missing = [] + for k in openid_args['signed'].split(','): + if not ("openid."+k) in m.toPostArgs().keys(): + missing.append(k) + self.assertEqual([], missing, missing) + self.assertEqual(openid_args, m.toArgs()) + self.failUnless(m.isOpenID1()) + + def test_112B(self): + args = {'openid.assoc_handle': 'fa1f5ff0-cde4-11dc-a183-3714bfd55ca8', + 'openid.claimed_id': 'http://binkley.lan/user/test01', + 'openid.identity': 'http://test01.binkley.lan/', + 'openid.mode': 'id_res', + 'openid.ns': 'http://specs.openid.net/auth/2.0', + 'openid.ns.pape': 'http://specs.openid.net/extensions/pape/1.0', + 'openid.op_endpoint': 'http://binkley.lan/server', + 'openid.pape.auth_policies': 'none', + 'openid.pape.auth_time': '2008-01-28T20:42:36Z', + 'openid.pape.nist_auth_level': '0', + 'openid.response_nonce': '2008-01-28T21:07:04Z99Q=', + 'openid.return_to': 'http://binkley.lan:8001/process?janrain_nonce=2008-01-28T21%3A07%3A02Z0tMIKx', + 'openid.sig': 'YJlWH4U6SroB1HoPkmEKx9AyGGg=', + 'openid.signed': 'assoc_handle,identity,response_nonce,return_to,claimed_id,op_endpoint,pape.auth_time,ns.pape,pape.nist_auth_level,pape.auth_policies' + } + m = message.Message.fromPostArgs(args) + missing = [] + for k in args['openid.signed'].split(','): + if not ("openid."+k) in m.toPostArgs().keys(): + missing.append(k) + self.assertEqual([], missing, missing) + self.assertEqual(args, m.toPostArgs()) + self.failUnless(m.isOpenID2()) + + def test_implicit_sreg_ns(self): + openid_args = { + 'sreg.email': 'a@b.com' + } + m = message.Message.fromOpenIDArgs(openid_args) + self.failUnless((sreg.ns_uri, 'sreg') in + list(m.namespaces.iteritems())) + self.assertEqual('a@b.com', m.getArg(sreg.ns_uri, 'email')) + self.assertEqual(openid_args, m.toArgs()) + self.failUnless(m.isOpenID1()) + + def _test_delArgNS(self, ns): + key = 'Camper van Beethoven' + value = 'David Lowery' + + self.failUnlessRaises(KeyError, self.msg.delArg, ns, key) + self.msg.setArg(ns, key, value) + self.failUnlessEqual(self.msg.getArg(ns, key), value) + self.msg.delArg(ns, key) + self.failUnlessEqual(self.msg.getArg(ns, key), None) + + def test_delArgOpenID(self): + self._test_delArgNS(message.OPENID_NS) + + def test_delArgBARE(self): + self._test_delArgNS(message.BARE_NS) + + def test_delArgNS1(self): + self._test_delArgNS(message.OPENID1_NS) + + def test_delArgNS2(self): + self._test_delArgNS(message.OPENID2_NS) + + def test_delArgNS3(self): + self._test_delArgNS('urn:nothing-significant') + + def test_overwriteExtensionArg(self): + ns = 'urn:unittest_extension' + key = 'mykey' + value_1 = 'value_1' + value_2 = 'value_2' + + self.msg.setArg(ns, key, value_1) + self.failUnless(self.msg.getArg(ns, key) == value_1) + self.msg.setArg(ns, key, value_2) + self.failUnless(self.msg.getArg(ns, key) == value_2) + + def test_argList(self): + self.failUnlessRaises(TypeError, self.msg.fromPostArgs, + {'arg': [1, 2, 3]}) + + def test_isOpenID1(self): + self.failIf(self.msg.isOpenID1()) + + def test_isOpenID2(self): + self.failUnless(self.msg.isOpenID2()) + +class MessageTest(unittest.TestCase): + def setUp(self): + self.postargs = { + 'openid.ns': message.OPENID2_NS, + 'openid.mode': 'checkid_setup', + 'openid.identity': 'http://bogus.example.invalid:port/', + 'openid.assoc_handle': 'FLUB', + 'openid.return_to': 'Neverland', + } + + self.action_url = 'scheme://host:port/path?query' + + self.form_tag_attrs = { + 'company': 'janrain', + 'class': 'fancyCSS', + } + + self.submit_text = 'GO!' + + ### Expected data regardless of input + + self.required_form_attrs = { + 'accept-charset':'UTF-8', + 'enctype':'application/x-www-form-urlencoded', + 'method': 'post', + } + + def _checkForm(self, html, message_, action_url, + form_tag_attrs, submit_text): + E = oidutil.importElementTree() + + # Build element tree from HTML source + input_tree = E.ElementTree(E.fromstring(html)) + + # Get root element + form = input_tree.getroot() + + # Check required form attributes + for k, v in self.required_form_attrs.iteritems(): + assert form.attrib[k] == v, \ + "Expected '%s' for required form attribute '%s', got '%s'" % \ + (v, k, form.attrib[k]) + + # Check extra form attributes + for k, v in form_tag_attrs.iteritems(): + + # Skip attributes that already passed the required + # attribute check, since they should be ignored by the + # form generation code. + if k in self.required_form_attrs: + continue + + assert form.attrib[k] == v, \ + "Form attribute '%s' should be '%s', found '%s'" % \ + (k, v, form.attrib[k]) + + # Check hidden fields against post args + hiddens = [e for e in form \ + if e.tag.upper() == 'INPUT' and \ + e.attrib['type'].upper() == 'HIDDEN'] + + # For each post arg, make sure there is a hidden with that + # value. Make sure there are no other hiddens. + for name, value in message_.toPostArgs().iteritems(): + for e in hiddens: + if e.attrib['name'] == name: + assert e.attrib['value'] == value, \ + "Expected value of hidden input '%s' to be '%s', got '%s'" % \ + (e.attrib['name'], value, e.attrib['value']) + break + else: + self.fail("Post arg '%s' not found in form" % (name,)) + + for e in hiddens: + assert e.attrib['name'] in message_.toPostArgs().keys(), \ + "Form element for '%s' not in " + \ + "original message" % (e.attrib['name']) + + # Check action URL + assert form.attrib['action'] == action_url, \ + "Expected form 'action' to be '%s', got '%s'" % \ + (action_url, form.attrib['action']) + + # Check submit text + submits = [e for e in form \ + if e.tag.upper() == 'INPUT' and \ + e.attrib['type'].upper() == 'SUBMIT'] + + assert len(submits) == 1, \ + "Expected only one 'input' with type = 'submit', got %d" % \ + (len(submits),) + + assert submits[0].attrib['value'] == submit_text, \ + "Expected submit value to be '%s', got '%s'" % \ + (submit_text, submits[0].attrib['value']) + + def test_toFormMarkup(self): + m = message.Message.fromPostArgs(self.postargs) + html = m.toFormMarkup(self.action_url, self.form_tag_attrs, + self.submit_text) + self._checkForm(html, m, self.action_url, + self.form_tag_attrs, self.submit_text) + + def test_overrideMethod(self): + """Be sure that caller cannot change form method to GET.""" + m = message.Message.fromPostArgs(self.postargs) + + tag_attrs = dict(self.form_tag_attrs) + tag_attrs['method'] = 'GET' + + html = m.toFormMarkup(self.action_url, self.form_tag_attrs, + self.submit_text) + self._checkForm(html, m, self.action_url, + self.form_tag_attrs, self.submit_text) + + def test_overrideRequired(self): + """Be sure that caller CANNOT change the form charset for + encoding type.""" + m = message.Message.fromPostArgs(self.postargs) + + tag_attrs = dict(self.form_tag_attrs) + tag_attrs['accept-charset'] = 'UCS4' + tag_attrs['enctype'] = 'invalid/x-broken' + + html = m.toFormMarkup(self.action_url, tag_attrs, + self.submit_text) + self._checkForm(html, m, self.action_url, + tag_attrs, self.submit_text) + + + def test_setOpenIDNamespace_invalid(self): + m = message.Message() + invalid_things = [ + # Empty string is not okay here. + '', + # Good guess! But wrong. + 'http://openid.net/signon/2.0', + # What? + u'http://specs%\\\r2Eopenid.net/auth/2.0', + # Too much escapings! + 'http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0', + # This is a Type URI, not a openid.ns value. + 'http://specs.openid.net/auth/2.0/signon', + ] + + for x in invalid_things: + self.failUnlessRaises(message.InvalidOpenIDNamespace, + m.setOpenIDNamespace, x, False) + + + def test_isOpenID1(self): + v1_namespaces = [ + # Yes, there are two of them. + 'http://openid.net/signon/1.1', + 'http://openid.net/signon/1.0', + ] + + for ns in v1_namespaces: + m = message.Message(ns) + self.failUnless(m.isOpenID1(), "%r not recognized as OpenID 1" % + (ns,)) + self.failUnlessEqual(ns, m.getOpenIDNamespace()) + self.failUnless(m.namespaces.isImplicit(ns), + m.namespaces.getNamespaceURI(message.NULL_NAMESPACE)) + + def test_isOpenID2(self): + ns = 'http://specs.openid.net/auth/2.0' + m = message.Message(ns) + self.failUnless(m.isOpenID2()) + self.failIf(m.namespaces.isImplicit(message.NULL_NAMESPACE)) + self.failUnlessEqual(ns, m.getOpenIDNamespace()) + + def test_setOpenIDNamespace_explicit(self): + m = message.Message() + m.setOpenIDNamespace(message.THE_OTHER_OPENID1_NS, False) + self.failIf(m.namespaces.isImplicit(message.THE_OTHER_OPENID1_NS)) + + def test_setOpenIDNamespace_implicit(self): + m = message.Message() + m.setOpenIDNamespace(message.THE_OTHER_OPENID1_NS, True) + self.failUnless(m.namespaces.isImplicit(message.THE_OTHER_OPENID1_NS)) + + + def test_explicitOpenID11NSSerialzation(self): + m = message.Message() + m.setOpenIDNamespace(message.THE_OTHER_OPENID1_NS, implicit=False) + + post_args = m.toPostArgs() + self.failUnlessEqual(post_args, + {'openid.ns':message.THE_OTHER_OPENID1_NS}) + + def test_fromPostArgs_ns11(self): + # An example of the stuff that some Drupal installations send us, + # which includes openid.ns but is 1.1. + query = { + u'openid.assoc_handle': u'', + u'openid.claimed_id': u'http://foobar.invalid/', + u'openid.identity': u'http://foobar.myopenid.com', + u'openid.mode': u'checkid_setup', + u'openid.ns': u'http://openid.net/signon/1.1', + u'openid.ns.sreg': u'http://openid.net/extensions/sreg/1.1', + u'openid.return_to': u'http://drupal.invalid/return_to', + u'openid.sreg.required': u'nickname,email', + u'openid.trust_root': u'http://drupal.invalid', + } + m = message.Message.fromPostArgs(query) + self.failUnless(m.isOpenID1()) + + + +class NamespaceMapTest(unittest.TestCase): + def test_onealias(self): + nsm = message.NamespaceMap() + uri = 'http://example.com/foo' + alias = "foo" + nsm.addAlias(uri, alias) + self.failUnless(nsm.getNamespaceURI(alias) == uri) + self.failUnless(nsm.getAlias(uri) == alias) + + def test_iteration(self): + nsm = message.NamespaceMap() + uripat = 'http://example.com/foo%r' + + nsm.add(uripat%0) + for n in range(1,23): + self.failUnless(uripat%(n-1) in nsm) + self.failUnless(nsm.isDefined(uripat%(n-1))) + nsm.add(uripat%n) + + for (uri, alias) in nsm.iteritems(): + self.failUnless(uri[22:]==alias[3:]) + + i=0 + it = nsm.iterAliases() + try: + while True: + it.next() + i += 1 + except StopIteration: + self.failUnless(i == 23) + + i=0 + it = nsm.iterNamespaceURIs() + try: + while True: + it.next() + i += 1 + except StopIteration: + self.failUnless(i == 23) + + +if __name__ == '__main__': + unittest.main() diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_negotiation.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_negotiation.py new file mode 100644 index 0000000..c9c254a --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_negotiation.py @@ -0,0 +1,271 @@ + +import unittest +from support import CatchLogs + +from openid.message import Message, OPENID2_NS, OPENID1_NS, OPENID_NS +from openid import association +from openid.consumer.consumer import GenericConsumer, ServerError +from openid.consumer.discover import OpenIDServiceEndpoint, OPENID_2_0_TYPE + +class ErrorRaisingConsumer(GenericConsumer): + """ + A consumer whose _requestAssocation will return predefined results + instead of trying to actually perform association requests. + """ + + # The list of objects to be returned by successive calls to + # _requestAssocation. Each call will pop the first element from + # this list and return it to _negotiateAssociation. If the + # element is a Message object, it will be wrapped in a ServerError + # exception. Otherwise it will be returned as-is. + return_messages = [] + + def _requestAssociation(self, endpoint, assoc_type, session_type): + m = self.return_messages.pop(0) + if isinstance(m, Message): + raise ServerError.fromMessage(m) + else: + return m + +class TestOpenID2SessionNegotiation(unittest.TestCase, CatchLogs): + """ + Test the session type negotiation behavior of an OpenID 2 + consumer. + """ + def setUp(self): + CatchLogs.setUp(self) + self.consumer = ErrorRaisingConsumer(store=None) + + self.endpoint = OpenIDServiceEndpoint() + self.endpoint.type_uris = [OPENID_2_0_TYPE] + self.endpoint.server_url = 'bogus' + + def testBadResponse(self): + """ + Test the case where the response to an associate request is a + server error or is otherwise undecipherable. + """ + self.consumer.return_messages = [Message(self.endpoint.preferredNamespace())] + self.assertEqual(self.consumer._negotiateAssociation(self.endpoint), None) + self.failUnlessLogMatches('Server error when requesting an association') + + def testEmptyAssocType(self): + """ + Test the case where the association type (assoc_type) returned + in an unsupported-type response is absent. + """ + msg = Message(self.endpoint.preferredNamespace()) + msg.setArg(OPENID_NS, 'error', 'Unsupported type') + msg.setArg(OPENID_NS, 'error_code', 'unsupported-type') + # not set: msg.delArg(OPENID_NS, 'assoc_type') + msg.setArg(OPENID_NS, 'session_type', 'new-session-type') + + self.consumer.return_messages = [msg] + self.assertEqual(self.consumer._negotiateAssociation(self.endpoint), None) + + self.failUnlessLogMatches('Unsupported association type', + 'Server responded with unsupported association ' + + 'session but did not supply a fallback.') + + def testEmptySessionType(self): + """ + Test the case where the session type (session_type) returned + in an unsupported-type response is absent. + """ + msg = Message(self.endpoint.preferredNamespace()) + msg.setArg(OPENID_NS, 'error', 'Unsupported type') + msg.setArg(OPENID_NS, 'error_code', 'unsupported-type') + msg.setArg(OPENID_NS, 'assoc_type', 'new-assoc-type') + # not set: msg.setArg(OPENID_NS, 'session_type', None) + + self.consumer.return_messages = [msg] + self.assertEqual(self.consumer._negotiateAssociation(self.endpoint), None) + + self.failUnlessLogMatches('Unsupported association type', + 'Server responded with unsupported association ' + + 'session but did not supply a fallback.') + + def testNotAllowed(self): + """ + Test the case where an unsupported-type response specifies a + preferred (assoc_type, session_type) combination that is not + allowed by the consumer's SessionNegotiator. + """ + allowed_types = [] + + negotiator = association.SessionNegotiator(allowed_types) + self.consumer.negotiator = negotiator + + msg = Message(self.endpoint.preferredNamespace()) + msg.setArg(OPENID_NS, 'error', 'Unsupported type') + msg.setArg(OPENID_NS, 'error_code', 'unsupported-type') + msg.setArg(OPENID_NS, 'assoc_type', 'not-allowed') + msg.setArg(OPENID_NS, 'session_type', 'not-allowed') + + self.consumer.return_messages = [msg] + self.assertEqual(self.consumer._negotiateAssociation(self.endpoint), None) + + self.failUnlessLogMatches('Unsupported association type', + 'Server sent unsupported session/association type:') + + def testUnsupportedWithRetry(self): + """ + Test the case where an unsupported-type response triggers a + retry to get an association with the new preferred type. + """ + msg = Message(self.endpoint.preferredNamespace()) + msg.setArg(OPENID_NS, 'error', 'Unsupported type') + msg.setArg(OPENID_NS, 'error_code', 'unsupported-type') + msg.setArg(OPENID_NS, 'assoc_type', 'HMAC-SHA1') + msg.setArg(OPENID_NS, 'session_type', 'DH-SHA1') + + assoc = association.Association( + 'handle', 'secret', 'issued', 10000, 'HMAC-SHA1') + + self.consumer.return_messages = [msg, assoc] + self.failUnless(self.consumer._negotiateAssociation(self.endpoint) is assoc) + + self.failUnlessLogMatches('Unsupported association type') + + def testUnsupportedWithRetryAndFail(self): + """ + Test the case where an unsupported-typ response triggers a + retry, but the retry fails and None is returned instead. + """ + msg = Message(self.endpoint.preferredNamespace()) + msg.setArg(OPENID_NS, 'error', 'Unsupported type') + msg.setArg(OPENID_NS, 'error_code', 'unsupported-type') + msg.setArg(OPENID_NS, 'assoc_type', 'HMAC-SHA1') + msg.setArg(OPENID_NS, 'session_type', 'DH-SHA1') + + self.consumer.return_messages = [msg, + Message(self.endpoint.preferredNamespace())] + + self.failUnlessEqual(self.consumer._negotiateAssociation(self.endpoint), None) + + self.failUnlessLogMatches('Unsupported association type', + 'Server %s refused' % (self.endpoint.server_url)) + + def testValid(self): + """ + Test the valid case, wherein an association is returned on the + first attempt to get one. + """ + assoc = association.Association( + 'handle', 'secret', 'issued', 10000, 'HMAC-SHA1') + + self.consumer.return_messages = [assoc] + self.failUnless(self.consumer._negotiateAssociation(self.endpoint) is assoc) + self.failUnlessLogEmpty() + +class TestOpenID1SessionNegotiation(unittest.TestCase, CatchLogs): + """ + Tests for the OpenID 1 consumer association session behavior. See + the docs for TestOpenID2SessionNegotiation. Notice that this + class is not a subclass of the OpenID 2 tests. Instead, it uses + many of the same inputs but inspects the log messages logged with + oidutil.log. See the calls to self.failUnlessLogMatches. Some of + these tests pass openid2-style messages to the openid 1 + association processing logic to be sure it ignores the extra data. + """ + def setUp(self): + CatchLogs.setUp(self) + self.consumer = ErrorRaisingConsumer(store=None) + + self.endpoint = OpenIDServiceEndpoint() + self.endpoint.type_uris = [OPENID1_NS] + self.endpoint.server_url = 'bogus' + + def testBadResponse(self): + self.consumer.return_messages = [Message(self.endpoint.preferredNamespace())] + self.assertEqual(self.consumer._negotiateAssociation(self.endpoint), None) + self.failUnlessLogMatches('Server error when requesting an association') + + def testEmptyAssocType(self): + msg = Message(self.endpoint.preferredNamespace()) + msg.setArg(OPENID_NS, 'error', 'Unsupported type') + msg.setArg(OPENID_NS, 'error_code', 'unsupported-type') + # not set: msg.setArg(OPENID_NS, 'assoc_type', None) + msg.setArg(OPENID_NS, 'session_type', 'new-session-type') + + self.consumer.return_messages = [msg] + self.assertEqual(self.consumer._negotiateAssociation(self.endpoint), None) + + self.failUnlessLogMatches('Server error when requesting an association') + + def testEmptySessionType(self): + msg = Message(self.endpoint.preferredNamespace()) + msg.setArg(OPENID_NS, 'error', 'Unsupported type') + msg.setArg(OPENID_NS, 'error_code', 'unsupported-type') + msg.setArg(OPENID_NS, 'assoc_type', 'new-assoc-type') + # not set: msg.setArg(OPENID_NS, 'session_type', None) + + self.consumer.return_messages = [msg] + self.assertEqual(self.consumer._negotiateAssociation(self.endpoint), None) + + self.failUnlessLogMatches('Server error when requesting an association') + + def testNotAllowed(self): + allowed_types = [] + + negotiator = association.SessionNegotiator(allowed_types) + self.consumer.negotiator = negotiator + + msg = Message(self.endpoint.preferredNamespace()) + msg.setArg(OPENID_NS, 'error', 'Unsupported type') + msg.setArg(OPENID_NS, 'error_code', 'unsupported-type') + msg.setArg(OPENID_NS, 'assoc_type', 'not-allowed') + msg.setArg(OPENID_NS, 'session_type', 'not-allowed') + + self.consumer.return_messages = [msg] + self.assertEqual(self.consumer._negotiateAssociation(self.endpoint), None) + + self.failUnlessLogMatches('Server error when requesting an association') + + def testUnsupportedWithRetry(self): + msg = Message(self.endpoint.preferredNamespace()) + msg.setArg(OPENID_NS, 'error', 'Unsupported type') + msg.setArg(OPENID_NS, 'error_code', 'unsupported-type') + msg.setArg(OPENID_NS, 'assoc_type', 'HMAC-SHA1') + msg.setArg(OPENID_NS, 'session_type', 'DH-SHA1') + + assoc = association.Association( + 'handle', 'secret', 'issued', 10000, 'HMAC-SHA1') + + self.consumer.return_messages = [msg, assoc] + self.failUnless(self.consumer._negotiateAssociation(self.endpoint) is None) + + self.failUnlessLogMatches('Server error when requesting an association') + + def testValid(self): + assoc = association.Association( + 'handle', 'secret', 'issued', 10000, 'HMAC-SHA1') + + self.consumer.return_messages = [assoc] + self.failUnless(self.consumer._negotiateAssociation(self.endpoint) is assoc) + self.failUnlessLogEmpty() + +class TestNegotiatorBehaviors(unittest.TestCase, CatchLogs): + def setUp(self): + self.allowed_types = [ + ('HMAC-SHA1', 'no-encryption'), + ('HMAC-SHA256', 'no-encryption'), + ] + + self.n = association.SessionNegotiator(self.allowed_types) + + def testAddAllowedTypeNoSessionTypes(self): + self.assertRaises(ValueError, self.n.addAllowedType, 'invalid') + + def testAddAllowedTypeBadSessionType(self): + self.assertRaises(ValueError, self.n.addAllowedType, 'assoc1', 'invalid') + + def testAddAllowedTypeContents(self): + assoc_type = 'HMAC-SHA1' + self.failUnless(self.n.addAllowedType(assoc_type) is None) + + for typ in association.getSessionTypes(assoc_type): + self.failUnless((assoc_type, typ) in self.n.allowed_types) + +if __name__ == '__main__': + unittest.main() diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_nonce.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_nonce.py new file mode 100644 index 0000000..2138305 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_nonce.py @@ -0,0 +1,104 @@ +from openid.test import datadriven +import time +import unittest +import re + +from openid.store.nonce import \ + mkNonce, \ + split as splitNonce, \ + checkTimestamp + +nonce_re = re.compile(r'\A\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ') + +class NonceTest(unittest.TestCase): + def test_mkNonce(self): + nonce = mkNonce() + self.failUnless(nonce_re.match(nonce)) + self.failUnless(len(nonce) == 26) + + def test_mkNonce_when(self): + nonce = mkNonce(0) + self.failUnless(nonce_re.match(nonce)) + self.failUnless(nonce.startswith('1970-01-01T00:00:00Z')) + self.failUnless(len(nonce) == 26) + + def test_splitNonce(self): + s = '1970-01-01T00:00:00Z' + expected_t = 0 + expected_salt = '' + actual_t, actual_salt = splitNonce(s) + self.failUnlessEqual(expected_t, actual_t) + self.failUnlessEqual(expected_salt, actual_salt) + + def test_mkSplit(self): + t = 42 + nonce_str = mkNonce(t) + self.failUnless(nonce_re.match(nonce_str)) + et, salt = splitNonce(nonce_str) + self.failUnlessEqual(len(salt), 6) + self.failUnlessEqual(et, t) + +class BadSplitTest(datadriven.DataDrivenTestCase): + cases = [ + '', + '1970-01-01T00:00:00+1:00', + '1969-01-01T00:00:00Z', + '1970-00-01T00:00:00Z', + '1970.01-01T00:00:00Z', + 'Thu Sep 7 13:29:31 PDT 2006', + 'monkeys', + ] + + def __init__(self, nonce_str): + datadriven.DataDrivenTestCase.__init__(self, nonce_str) + self.nonce_str = nonce_str + + def runOneTest(self): + self.failUnlessRaises(ValueError, splitNonce, self.nonce_str) + +class CheckTimestampTest(datadriven.DataDrivenTestCase): + cases = [ + # exact, no allowed skew + ('1970-01-01T00:00:00Z', 0, 0, True), + + # exact, large skew + ('1970-01-01T00:00:00Z', 1000, 0, True), + + # no allowed skew, one second old + ('1970-01-01T00:00:00Z', 0, 1, False), + + # many seconds old, outside of skew + ('1970-01-01T00:00:00Z', 10, 50, False), + + # one second old, one second skew allowed + ('1970-01-01T00:00:00Z', 1, 1, True), + + # One second in the future, one second skew allowed + ('1970-01-01T00:00:02Z', 1, 1, True), + + # two seconds in the future, one second skew allowed + ('1970-01-01T00:00:02Z', 1, 0, False), + + # malformed nonce string + ('monkeys', 0, 0, False), + ] + + def __init__(self, nonce_string, allowed_skew, now, expected): + datadriven.DataDrivenTestCase.__init__( + self, repr((nonce_string, allowed_skew, now))) + self.nonce_string = nonce_string + self.allowed_skew = allowed_skew + self.now = now + self.expected = expected + + def runOneTest(self): + actual = checkTimestamp(self.nonce_string, self.allowed_skew, self.now) + self.failUnlessEqual(bool(self.expected), bool(actual)) + +def pyUnitTests(): + return datadriven.loadTests(__name__) + +if __name__ == '__main__': + suite = pyUnitTests() + runner = unittest.TextTestRunner() + runner.run(suite) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_openidyadis.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_openidyadis.py new file mode 100644 index 0000000..8573d3c --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_openidyadis.py @@ -0,0 +1,164 @@ +import unittest +from openid.consumer.discover import \ + OpenIDServiceEndpoint, OPENID_1_1_TYPE, OPENID_1_0_TYPE + +from openid.yadis.services import applyFilter + + +XRDS_BOILERPLATE = '''\ + + + +%s\ + + +''' + +def mkXRDS(services): + return XRDS_BOILERPLATE % (services,) + +def mkService(uris=None, type_uris=None, local_id=None, dent=' '): + chunks = [dent, '\n'] + dent2 = dent + ' ' + if type_uris: + for type_uri in type_uris: + chunks.extend([dent2 + '', type_uri, '\n']) + + if uris: + for uri in uris: + if type(uri) is tuple: + uri, prio = uri + else: + prio = None + + chunks.extend([dent2, '', uri, '\n']) + + if local_id: + chunks.extend( + [dent2, '', local_id, '\n']) + + chunks.extend([dent, '\n']) + + return ''.join(chunks) + +# Different sets of server URLs for use in the URI tag +server_url_options = [ + [], # This case should not generate an endpoint object + ['http://server.url/'], + ['https://server.url/'], + ['https://server.url/', 'http://server.url/'], + ['https://server.url/', + 'http://server.url/', + 'http://example.server.url/'], + ] + +# Used for generating test data +def subsets(l): + """Generate all non-empty sublists of a list""" + subsets_list = [[]] + for x in l: + subsets_list += [[x] + t for t in subsets_list] + return subsets_list + +# A couple of example extension type URIs. These are not at all +# official, but are just here for testing. +ext_types = [ + 'http://janrain.com/extension/blah', + 'http://openid.net/sreg/1.0', + ] + +# All valid combinations of Type tags that should produce an OpenID endpoint +type_uri_options = [ + exts + ts + + # All non-empty sublists of the valid OpenID type URIs + for ts in subsets([OPENID_1_0_TYPE, OPENID_1_1_TYPE]) + if ts + + # All combinations of extension types (including empty extenstion list) + for exts in subsets(ext_types) + ] + +# Range of valid Delegate tag values for generating test data +local_id_options = [ + None, + 'http://vanity.domain/', + 'https://somewhere/yadis/', + ] + +# All combinations of valid URIs, Type URIs and Delegate tags +data = [ + (uris, type_uris, local_id) + for uris in server_url_options + for type_uris in type_uri_options + for local_id in local_id_options + ] + +class OpenIDYadisTest(unittest.TestCase): + def __init__(self, uris, type_uris, local_id): + unittest.TestCase.__init__(self) + self.uris = uris + self.type_uris = type_uris + self.local_id = local_id + + def shortDescription(self): + # XXX: + return 'Successful OpenID Yadis parsing case' + + def setUp(self): + self.yadis_url = 'http://unit.test/' + + # Create an XRDS document to parse + services = mkService(uris=self.uris, + type_uris=self.type_uris, + local_id=self.local_id) + self.xrds = mkXRDS(services) + + def runTest(self): + # Parse into endpoint objects that we will check + endpoints = applyFilter( + self.yadis_url, self.xrds, OpenIDServiceEndpoint) + + # make sure there are the same number of endpoints as + # URIs. This assumes that the type_uris contains at least one + # OpenID type. + self.failUnlessEqual(len(self.uris), len(endpoints)) + + # So that we can check equality on the endpoint types + type_uris = list(self.type_uris) + type_uris.sort() + + seen_uris = [] + for endpoint in endpoints: + seen_uris.append(endpoint.server_url) + + # All endpoints will have same yadis_url + self.failUnlessEqual(self.yadis_url, endpoint.claimed_id) + + # and local_id + self.failUnlessEqual(self.local_id, endpoint.local_id) + + # and types + actual_types = list(endpoint.type_uris) + actual_types.sort() + self.failUnlessEqual(actual_types, type_uris) + + # So that they will compare equal, because we don't care what + # order they are in + seen_uris.sort() + uris = list(self.uris) + uris.sort() + + # Make sure we saw all URIs, and saw each one once + self.failUnlessEqual(uris, seen_uris) + +def pyUnitTests(): + cases = [] + for args in data: + cases.append(OpenIDYadisTest(*args)) + return unittest.TestSuite(cases) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_pape.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_pape.py new file mode 100644 index 0000000..ef47f60 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_pape.py @@ -0,0 +1,9 @@ + +from openid.extensions import pape + +import unittest + +class PapeImportTestCase(unittest.TestCase): + def test_version(self): + from openid.extensions.draft import pape5 + self.assert_(pape is pape5) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_pape_draft2.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_pape_draft2.py new file mode 100644 index 0000000..ed3d439 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_pape_draft2.py @@ -0,0 +1,217 @@ + +from openid.extensions.draft import pape2 as pape +from openid.message import * +from openid.server import server + +import unittest + +class PapeRequestTestCase(unittest.TestCase): + def setUp(self): + self.req = pape.Request() + + def test_construct(self): + self.failUnlessEqual([], self.req.preferred_auth_policies) + self.failUnlessEqual(None, self.req.max_auth_age) + self.failUnlessEqual('pape', self.req.ns_alias) + + req2 = pape.Request([pape.AUTH_MULTI_FACTOR], 1000) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR], req2.preferred_auth_policies) + self.failUnlessEqual(1000, req2.max_auth_age) + + def test_add_policy_uri(self): + self.failUnlessEqual([], self.req.preferred_auth_policies) + self.req.addPolicyURI(pape.AUTH_MULTI_FACTOR) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR], self.req.preferred_auth_policies) + self.req.addPolicyURI(pape.AUTH_MULTI_FACTOR) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR], self.req.preferred_auth_policies) + self.req.addPolicyURI(pape.AUTH_PHISHING_RESISTANT) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR, pape.AUTH_PHISHING_RESISTANT], + self.req.preferred_auth_policies) + self.req.addPolicyURI(pape.AUTH_MULTI_FACTOR) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR, pape.AUTH_PHISHING_RESISTANT], + self.req.preferred_auth_policies) + + def test_getExtensionArgs(self): + self.failUnlessEqual({'preferred_auth_policies': ''}, self.req.getExtensionArgs()) + self.req.addPolicyURI('http://uri') + self.failUnlessEqual({'preferred_auth_policies': 'http://uri'}, self.req.getExtensionArgs()) + self.req.addPolicyURI('http://zig') + self.failUnlessEqual({'preferred_auth_policies': 'http://uri http://zig'}, self.req.getExtensionArgs()) + self.req.max_auth_age = 789 + self.failUnlessEqual({'preferred_auth_policies': 'http://uri http://zig', 'max_auth_age': '789'}, self.req.getExtensionArgs()) + + def test_parseExtensionArgs(self): + args = {'preferred_auth_policies': 'http://foo http://bar', + 'max_auth_age': '9'} + self.req.parseExtensionArgs(args) + self.failUnlessEqual(9, self.req.max_auth_age) + self.failUnlessEqual(['http://foo','http://bar'], self.req.preferred_auth_policies) + + def test_parseExtensionArgs_empty(self): + self.req.parseExtensionArgs({}) + self.failUnlessEqual(None, self.req.max_auth_age) + self.failUnlessEqual([], self.req.preferred_auth_policies) + + def test_fromOpenIDRequest(self): + openid_req_msg = Message.fromOpenIDArgs({ + 'mode': 'checkid_setup', + 'ns': OPENID2_NS, + 'ns.pape': pape.ns_uri, + 'pape.preferred_auth_policies': ' '.join([pape.AUTH_MULTI_FACTOR, pape.AUTH_PHISHING_RESISTANT]), + 'pape.max_auth_age': '5476' + }) + oid_req = server.OpenIDRequest() + oid_req.message = openid_req_msg + req = pape.Request.fromOpenIDRequest(oid_req) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR, pape.AUTH_PHISHING_RESISTANT], req.preferred_auth_policies) + self.failUnlessEqual(5476, req.max_auth_age) + + def test_fromOpenIDRequest_no_pape(self): + message = Message() + openid_req = server.OpenIDRequest() + openid_req.message = message + pape_req = pape.Request.fromOpenIDRequest(openid_req) + assert(pape_req is None) + + def test_preferred_types(self): + self.req.addPolicyURI(pape.AUTH_PHISHING_RESISTANT) + self.req.addPolicyURI(pape.AUTH_MULTI_FACTOR) + pt = self.req.preferredTypes([pape.AUTH_MULTI_FACTOR, + pape.AUTH_MULTI_FACTOR_PHYSICAL]) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR], pt) + +class DummySuccessResponse: + def __init__(self, message, signed_stuff): + self.message = message + self.signed_stuff = signed_stuff + + def getSignedNS(self, ns_uri): + return self.signed_stuff + +class PapeResponseTestCase(unittest.TestCase): + def setUp(self): + self.req = pape.Response() + + def test_construct(self): + self.failUnlessEqual([], self.req.auth_policies) + self.failUnlessEqual(None, self.req.auth_time) + self.failUnlessEqual('pape', self.req.ns_alias) + self.failUnlessEqual(None, self.req.nist_auth_level) + + req2 = pape.Response([pape.AUTH_MULTI_FACTOR], "2004-12-11T10:30:44Z", 3) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR], req2.auth_policies) + self.failUnlessEqual("2004-12-11T10:30:44Z", req2.auth_time) + self.failUnlessEqual(3, req2.nist_auth_level) + + def test_add_policy_uri(self): + self.failUnlessEqual([], self.req.auth_policies) + self.req.addPolicyURI(pape.AUTH_MULTI_FACTOR) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR], self.req.auth_policies) + self.req.addPolicyURI(pape.AUTH_MULTI_FACTOR) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR], self.req.auth_policies) + self.req.addPolicyURI(pape.AUTH_PHISHING_RESISTANT) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR, pape.AUTH_PHISHING_RESISTANT], self.req.auth_policies) + self.req.addPolicyURI(pape.AUTH_MULTI_FACTOR) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR, pape.AUTH_PHISHING_RESISTANT], self.req.auth_policies) + + def test_getExtensionArgs(self): + self.failUnlessEqual({'auth_policies': 'none'}, self.req.getExtensionArgs()) + self.req.addPolicyURI('http://uri') + self.failUnlessEqual({'auth_policies': 'http://uri'}, self.req.getExtensionArgs()) + self.req.addPolicyURI('http://zig') + self.failUnlessEqual({'auth_policies': 'http://uri http://zig'}, self.req.getExtensionArgs()) + self.req.auth_time = "1776-07-04T14:43:12Z" + self.failUnlessEqual({'auth_policies': 'http://uri http://zig', 'auth_time': "1776-07-04T14:43:12Z"}, self.req.getExtensionArgs()) + self.req.nist_auth_level = 3 + self.failUnlessEqual({'auth_policies': 'http://uri http://zig', 'auth_time': "1776-07-04T14:43:12Z", 'nist_auth_level': '3'}, self.req.getExtensionArgs()) + + def test_getExtensionArgs_error_auth_age(self): + self.req.auth_time = "long ago" + self.failUnlessRaises(ValueError, self.req.getExtensionArgs) + + def test_getExtensionArgs_error_nist_auth_level(self): + self.req.nist_auth_level = "high as a kite" + self.failUnlessRaises(ValueError, self.req.getExtensionArgs) + self.req.nist_auth_level = 5 + self.failUnlessRaises(ValueError, self.req.getExtensionArgs) + self.req.nist_auth_level = -1 + self.failUnlessRaises(ValueError, self.req.getExtensionArgs) + + def test_parseExtensionArgs(self): + args = {'auth_policies': 'http://foo http://bar', + 'auth_time': '1970-01-01T00:00:00Z'} + self.req.parseExtensionArgs(args) + self.failUnlessEqual('1970-01-01T00:00:00Z', self.req.auth_time) + self.failUnlessEqual(['http://foo','http://bar'], self.req.auth_policies) + + def test_parseExtensionArgs_empty(self): + self.req.parseExtensionArgs({}) + self.failUnlessEqual(None, self.req.auth_time) + self.failUnlessEqual([], self.req.auth_policies) + + def test_parseExtensionArgs_strict_bogus1(self): + args = {'auth_policies': 'http://foo http://bar', + 'auth_time': 'yesterday'} + self.failUnlessRaises(ValueError, self.req.parseExtensionArgs, + args, True) + + def test_parseExtensionArgs_strict_bogus2(self): + args = {'auth_policies': 'http://foo http://bar', + 'auth_time': '1970-01-01T00:00:00Z', + 'nist_auth_level': 'some'} + self.failUnlessRaises(ValueError, self.req.parseExtensionArgs, + args, True) + + def test_parseExtensionArgs_strict_good(self): + args = {'auth_policies': 'http://foo http://bar', + 'auth_time': '1970-01-01T00:00:00Z', + 'nist_auth_level': '0'} + self.req.parseExtensionArgs(args, True) + self.failUnlessEqual(['http://foo','http://bar'], self.req.auth_policies) + self.failUnlessEqual('1970-01-01T00:00:00Z', self.req.auth_time) + self.failUnlessEqual(0, self.req.nist_auth_level) + + def test_parseExtensionArgs_nostrict_bogus(self): + args = {'auth_policies': 'http://foo http://bar', + 'auth_time': 'when the cows come home', + 'nist_auth_level': 'some'} + self.req.parseExtensionArgs(args) + self.failUnlessEqual(['http://foo','http://bar'], self.req.auth_policies) + self.failUnlessEqual(None, self.req.auth_time) + self.failUnlessEqual(None, self.req.nist_auth_level) + + def test_fromSuccessResponse(self): + openid_req_msg = Message.fromOpenIDArgs({ + 'mode': 'id_res', + 'ns': OPENID2_NS, + 'ns.pape': pape.ns_uri, + 'pape.auth_policies': ' '.join([pape.AUTH_MULTI_FACTOR, pape.AUTH_PHISHING_RESISTANT]), + 'pape.auth_time': '1970-01-01T00:00:00Z' + }) + signed_stuff = { + 'auth_policies': ' '.join([pape.AUTH_MULTI_FACTOR, pape.AUTH_PHISHING_RESISTANT]), + 'auth_time': '1970-01-01T00:00:00Z' + } + oid_req = DummySuccessResponse(openid_req_msg, signed_stuff) + req = pape.Response.fromSuccessResponse(oid_req) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR, pape.AUTH_PHISHING_RESISTANT], req.auth_policies) + self.failUnlessEqual('1970-01-01T00:00:00Z', req.auth_time) + + def test_fromSuccessResponseNoSignedArgs(self): + openid_req_msg = Message.fromOpenIDArgs({ + 'mode': 'id_res', + 'ns': OPENID2_NS, + 'ns.pape': pape.ns_uri, + 'pape.auth_policies': ' '.join([pape.AUTH_MULTI_FACTOR, pape.AUTH_PHISHING_RESISTANT]), + 'pape.auth_time': '1970-01-01T00:00:00Z' + }) + + signed_stuff = {} + + class NoSigningDummyResponse(DummySuccessResponse): + def getSignedNS(self, ns_uri): + return None + + oid_req = NoSigningDummyResponse(openid_req_msg, signed_stuff) + resp = pape.Response.fromSuccessResponse(oid_req) + self.failUnless(resp is None) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_pape_draft5.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_pape_draft5.py new file mode 100644 index 0000000..d93ee96 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_pape_draft5.py @@ -0,0 +1,441 @@ + +from openid.extensions.draft import pape5 as pape +from openid.message import * +from openid.server import server + +import warnings +warnings.filterwarnings('ignore', module=__name__, + message='"none" used as a policy URI') + +import unittest + +class PapeRequestTestCase(unittest.TestCase): + def setUp(self): + self.req = pape.Request() + + def test_construct(self): + self.failUnlessEqual([], self.req.preferred_auth_policies) + self.failUnlessEqual(None, self.req.max_auth_age) + self.failUnlessEqual('pape', self.req.ns_alias) + self.failIf(self.req.preferred_auth_level_types) + + bogus_levels = ['http://janrain.com/our_levels'] + req2 = pape.Request( + [pape.AUTH_MULTI_FACTOR], 1000, bogus_levels) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR], + req2.preferred_auth_policies) + self.failUnlessEqual(1000, req2.max_auth_age) + self.failUnlessEqual(bogus_levels, req2.preferred_auth_level_types) + + def test_addAuthLevel(self): + self.req.addAuthLevel('http://example.com/', 'example') + self.failUnlessEqual(['http://example.com/'], + self.req.preferred_auth_level_types) + self.failUnlessEqual('http://example.com/', + self.req.auth_level_aliases['example']) + + self.req.addAuthLevel('http://example.com/1', 'example1') + self.failUnlessEqual(['http://example.com/', 'http://example.com/1'], + self.req.preferred_auth_level_types) + + self.req.addAuthLevel('http://example.com/', 'exmpl') + self.failUnlessEqual(['http://example.com/', 'http://example.com/1'], + self.req.preferred_auth_level_types) + + self.req.addAuthLevel('http://example.com/', 'example') + self.failUnlessEqual(['http://example.com/', 'http://example.com/1'], + self.req.preferred_auth_level_types) + + self.failUnlessRaises(KeyError, + self.req.addAuthLevel, + 'http://example.com/2', 'example') + + # alias is None; we expect a new one to be generated. + uri = 'http://another.example.com/' + self.req.addAuthLevel(uri) + self.assert_(uri in self.req.auth_level_aliases.values()) + + # We don't expect a new alias to be generated if one already + # exists. + before_aliases = self.req.auth_level_aliases.keys() + self.req.addAuthLevel(uri) + after_aliases = self.req.auth_level_aliases.keys() + self.assertEqual(before_aliases, after_aliases) + + def test_add_policy_uri(self): + self.failUnlessEqual([], self.req.preferred_auth_policies) + self.req.addPolicyURI(pape.AUTH_MULTI_FACTOR) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR], + self.req.preferred_auth_policies) + self.req.addPolicyURI(pape.AUTH_MULTI_FACTOR) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR], + self.req.preferred_auth_policies) + self.req.addPolicyURI(pape.AUTH_PHISHING_RESISTANT) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR, + pape.AUTH_PHISHING_RESISTANT], + self.req.preferred_auth_policies) + self.req.addPolicyURI(pape.AUTH_MULTI_FACTOR) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR, + pape.AUTH_PHISHING_RESISTANT], + self.req.preferred_auth_policies) + + def test_getExtensionArgs(self): + self.failUnlessEqual({'preferred_auth_policies': ''}, + self.req.getExtensionArgs()) + self.req.addPolicyURI('http://uri') + self.failUnlessEqual( + {'preferred_auth_policies': 'http://uri'}, + self.req.getExtensionArgs()) + self.req.addPolicyURI('http://zig') + self.failUnlessEqual( + {'preferred_auth_policies': 'http://uri http://zig'}, + self.req.getExtensionArgs()) + self.req.max_auth_age = 789 + self.failUnlessEqual( + {'preferred_auth_policies': 'http://uri http://zig', + 'max_auth_age': '789'}, + self.req.getExtensionArgs()) + + def test_getExtensionArgsWithAuthLevels(self): + uri = 'http://example.com/auth_level' + alias = 'my_level' + self.req.addAuthLevel(uri, alias) + + uri2 = 'http://example.com/auth_level_2' + alias2 = 'my_level_2' + self.req.addAuthLevel(uri2, alias2) + + expected_args = { + ('auth_level.ns.%s' % alias): uri, + ('auth_level.ns.%s' % alias2): uri2, + 'preferred_auth_level_types': ' '.join([alias, alias2]), + 'preferred_auth_policies': '', + } + + self.failUnlessEqual(expected_args, self.req.getExtensionArgs()) + + def test_parseExtensionArgsWithAuthLevels(self): + uri = 'http://example.com/auth_level' + alias = 'my_level' + + uri2 = 'http://example.com/auth_level_2' + alias2 = 'my_level_2' + + request_args = { + ('auth_level.ns.%s' % alias): uri, + ('auth_level.ns.%s' % alias2): uri2, + 'preferred_auth_level_types': ' '.join([alias, alias2]), + 'preferred_auth_policies': '', + } + + # Check request object state + self.req.parseExtensionArgs(request_args, is_openid1=False, strict=False) + + expected_auth_levels = [uri, uri2] + + self.assertEqual(expected_auth_levels, + self.req.preferred_auth_level_types) + self.assertEqual(uri, self.req.auth_level_aliases[alias]) + self.assertEqual(uri2, self.req.auth_level_aliases[alias2]) + + def test_parseExtensionArgsWithAuthLevels_openID1(self): + request_args = { + 'preferred_auth_level_types':'nist jisa', + } + expected_auth_levels = [pape.LEVELS_NIST, pape.LEVELS_JISA] + self.req.parseExtensionArgs(request_args, is_openid1=True) + self.assertEqual(expected_auth_levels, + self.req.preferred_auth_level_types) + + self.req = pape.Request() + self.req.parseExtensionArgs(request_args, is_openid1=False) + self.assertEqual([], + self.req.preferred_auth_level_types) + + self.req = pape.Request() + self.failUnlessRaises(ValueError, + self.req.parseExtensionArgs, + request_args, is_openid1=False, strict=True) + + def test_parseExtensionArgs_ignoreBadAuthLevels(self): + request_args = {'preferred_auth_level_types':'monkeys'} + self.req.parseExtensionArgs(request_args, False) + self.assertEqual([], self.req.preferred_auth_level_types) + + def test_parseExtensionArgs_strictBadAuthLevels(self): + request_args = {'preferred_auth_level_types':'monkeys'} + self.failUnlessRaises(ValueError, self.req.parseExtensionArgs, + request_args, is_openid1=False, strict=True) + + def test_parseExtensionArgs(self): + args = {'preferred_auth_policies': 'http://foo http://bar', + 'max_auth_age': '9'} + self.req.parseExtensionArgs(args, False) + self.failUnlessEqual(9, self.req.max_auth_age) + self.failUnlessEqual(['http://foo','http://bar'], + self.req.preferred_auth_policies) + self.failUnlessEqual([], self.req.preferred_auth_level_types) + + def test_parseExtensionArgs_strict_bad_auth_age(self): + args = {'max_auth_age': 'not an int'} + self.assertRaises(ValueError, self.req.parseExtensionArgs, args, + is_openid1=False, strict=True) + + def test_parseExtensionArgs_empty(self): + self.req.parseExtensionArgs({}, False) + self.failUnlessEqual(None, self.req.max_auth_age) + self.failUnlessEqual([], self.req.preferred_auth_policies) + self.failUnlessEqual([], self.req.preferred_auth_level_types) + + def test_fromOpenIDRequest(self): + policy_uris = [pape.AUTH_MULTI_FACTOR, pape.AUTH_PHISHING_RESISTANT] + openid_req_msg = Message.fromOpenIDArgs({ + 'mode': 'checkid_setup', + 'ns': OPENID2_NS, + 'ns.pape': pape.ns_uri, + 'pape.preferred_auth_policies': ' '.join(policy_uris), + 'pape.max_auth_age': '5476' + }) + oid_req = server.OpenIDRequest() + oid_req.message = openid_req_msg + req = pape.Request.fromOpenIDRequest(oid_req) + self.failUnlessEqual(policy_uris, req.preferred_auth_policies) + self.failUnlessEqual(5476, req.max_auth_age) + + def test_fromOpenIDRequest_no_pape(self): + message = Message() + openid_req = server.OpenIDRequest() + openid_req.message = message + pape_req = pape.Request.fromOpenIDRequest(openid_req) + assert(pape_req is None) + + def test_preferred_types(self): + self.req.addPolicyURI(pape.AUTH_PHISHING_RESISTANT) + self.req.addPolicyURI(pape.AUTH_MULTI_FACTOR) + pt = self.req.preferredTypes([pape.AUTH_MULTI_FACTOR, + pape.AUTH_MULTI_FACTOR_PHYSICAL]) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR], pt) + +class DummySuccessResponse: + def __init__(self, message, signed_stuff): + self.message = message + self.signed_stuff = signed_stuff + + def isOpenID1(self): + return False + + def getSignedNS(self, ns_uri): + return self.signed_stuff + +class PapeResponseTestCase(unittest.TestCase): + def setUp(self): + self.resp = pape.Response() + + def test_construct(self): + self.failUnlessEqual([], self.resp.auth_policies) + self.failUnlessEqual(None, self.resp.auth_time) + self.failUnlessEqual('pape', self.resp.ns_alias) + self.failUnlessEqual(None, self.resp.nist_auth_level) + + req2 = pape.Response([pape.AUTH_MULTI_FACTOR], + "2004-12-11T10:30:44Z", {pape.LEVELS_NIST: 3}) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR], req2.auth_policies) + self.failUnlessEqual("2004-12-11T10:30:44Z", req2.auth_time) + self.failUnlessEqual(3, req2.nist_auth_level) + + def test_add_policy_uri(self): + self.failUnlessEqual([], self.resp.auth_policies) + self.resp.addPolicyURI(pape.AUTH_MULTI_FACTOR) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR], self.resp.auth_policies) + self.resp.addPolicyURI(pape.AUTH_MULTI_FACTOR) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR], self.resp.auth_policies) + self.resp.addPolicyURI(pape.AUTH_PHISHING_RESISTANT) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR, + pape.AUTH_PHISHING_RESISTANT], + self.resp.auth_policies) + self.resp.addPolicyURI(pape.AUTH_MULTI_FACTOR) + self.failUnlessEqual([pape.AUTH_MULTI_FACTOR, + pape.AUTH_PHISHING_RESISTANT], + self.resp.auth_policies) + + self.failUnlessRaises(RuntimeError, self.resp.addPolicyURI, + pape.AUTH_NONE) + + def test_getExtensionArgs(self): + self.failUnlessEqual({'auth_policies': pape.AUTH_NONE}, + self.resp.getExtensionArgs()) + self.resp.addPolicyURI('http://uri') + self.failUnlessEqual({'auth_policies': 'http://uri'}, + self.resp.getExtensionArgs()) + self.resp.addPolicyURI('http://zig') + self.failUnlessEqual({'auth_policies': 'http://uri http://zig'}, + self.resp.getExtensionArgs()) + self.resp.auth_time = "1776-07-04T14:43:12Z" + self.failUnlessEqual( + {'auth_policies': 'http://uri http://zig', + 'auth_time': "1776-07-04T14:43:12Z"}, + self.resp.getExtensionArgs()) + self.resp.setAuthLevel(pape.LEVELS_NIST, '3') + self.failUnlessEqual( + {'auth_policies': 'http://uri http://zig', + 'auth_time': "1776-07-04T14:43:12Z", + 'auth_level.nist': '3', + 'auth_level.ns.nist': pape.LEVELS_NIST}, + self.resp.getExtensionArgs()) + + def test_getExtensionArgs_error_auth_age(self): + self.resp.auth_time = "long ago" + self.failUnlessRaises(ValueError, self.resp.getExtensionArgs) + + def test_parseExtensionArgs(self): + args = {'auth_policies': 'http://foo http://bar', + 'auth_time': '1970-01-01T00:00:00Z'} + self.resp.parseExtensionArgs(args, is_openid1=False) + self.failUnlessEqual('1970-01-01T00:00:00Z', self.resp.auth_time) + self.failUnlessEqual(['http://foo','http://bar'], + self.resp.auth_policies) + + def test_parseExtensionArgs_valid_none(self): + args = {'auth_policies': pape.AUTH_NONE} + self.resp.parseExtensionArgs(args, is_openid1=False) + self.failUnlessEqual([], self.resp.auth_policies) + + def test_parseExtensionArgs_old_none(self): + args = {'auth_policies': 'none'} + self.resp.parseExtensionArgs(args, is_openid1=False) + self.failUnlessEqual([], self.resp.auth_policies) + + def test_parseExtensionArgs_old_none_strict(self): + args = {'auth_policies': 'none'} + self.failUnlessRaises( + ValueError, + self.resp.parseExtensionArgs, args, is_openid1=False, strict=True) + + def test_parseExtensionArgs_empty(self): + self.resp.parseExtensionArgs({}, is_openid1=False) + self.failUnlessEqual(None, self.resp.auth_time) + self.failUnlessEqual([], self.resp.auth_policies) + + def test_parseExtensionArgs_empty_strict(self): + self.failUnlessRaises( + ValueError, + self.resp.parseExtensionArgs, {}, is_openid1=False, strict=True) + + def test_parseExtensionArgs_ignore_superfluous_none(self): + policies = [pape.AUTH_NONE, pape.AUTH_MULTI_FACTOR_PHYSICAL] + + args = { + 'auth_policies': ' '.join(policies), + } + + self.resp.parseExtensionArgs(args, is_openid1=False, strict=False) + + self.assertEqual([pape.AUTH_MULTI_FACTOR_PHYSICAL], + self.resp.auth_policies) + + def test_parseExtensionArgs_none_strict(self): + policies = [pape.AUTH_NONE, pape.AUTH_MULTI_FACTOR_PHYSICAL] + + args = { + 'auth_policies': ' '.join(policies), + } + + self.failUnlessRaises(ValueError, self.resp.parseExtensionArgs, + args, is_openid1=False, strict=True) + + def test_parseExtensionArgs_strict_bogus1(self): + args = {'auth_policies': 'http://foo http://bar', + 'auth_time': 'yesterday'} + self.failUnlessRaises(ValueError, self.resp.parseExtensionArgs, + args, is_openid1=False, strict=True) + + def test_parseExtensionArgs_openid1_strict(self): + args = {'auth_level.nist': '0', + 'auth_policies': pape.AUTH_NONE, + } + self.resp.parseExtensionArgs(args, strict=True, is_openid1=True) + self.failUnlessEqual('0', self.resp.getAuthLevel(pape.LEVELS_NIST)) + self.failUnlessEqual([], self.resp.auth_policies) + + def test_parseExtensionArgs_strict_no_namespace_decl_openid2(self): + # Test the case where the namespace is not declared for an + # auth level. + args = {'auth_policies': pape.AUTH_NONE, + 'auth_level.nist': '0', + } + self.failUnlessRaises(ValueError, self.resp.parseExtensionArgs, + args, is_openid1=False, strict=True) + + def test_parseExtensionArgs_nostrict_no_namespace_decl_openid2(self): + # Test the case where the namespace is not declared for an + # auth level. + args = {'auth_policies': pape.AUTH_NONE, + 'auth_level.nist': '0', + } + self.resp.parseExtensionArgs(args, is_openid1=False, strict=False) + + # There is no namespace declaration for this auth level. + self.failUnlessRaises(KeyError, self.resp.getAuthLevel, + pape.LEVELS_NIST) + + def test_parseExtensionArgs_strict_good(self): + args = {'auth_policies': 'http://foo http://bar', + 'auth_time': '1970-01-01T00:00:00Z', + 'auth_level.nist': '0', + 'auth_level.ns.nist': pape.LEVELS_NIST} + self.resp.parseExtensionArgs(args, is_openid1=False, strict=True) + self.failUnlessEqual(['http://foo','http://bar'], + self.resp.auth_policies) + self.failUnlessEqual('1970-01-01T00:00:00Z', self.resp.auth_time) + self.failUnlessEqual(0, self.resp.nist_auth_level) + + def test_parseExtensionArgs_nostrict_bogus(self): + args = {'auth_policies': 'http://foo http://bar', + 'auth_time': 'when the cows come home', + 'nist_auth_level': 'some'} + self.resp.parseExtensionArgs(args, is_openid1=False) + self.failUnlessEqual(['http://foo','http://bar'], + self.resp.auth_policies) + self.failUnlessEqual(None, self.resp.auth_time) + self.failUnlessEqual(None, self.resp.nist_auth_level) + + def test_fromSuccessResponse(self): + policy_uris = [pape.AUTH_MULTI_FACTOR, pape.AUTH_PHISHING_RESISTANT] + openid_req_msg = Message.fromOpenIDArgs({ + 'mode': 'id_res', + 'ns': OPENID2_NS, + 'ns.pape': pape.ns_uri, + 'pape.auth_policies': ' '.join(policy_uris), + 'pape.auth_time': '1970-01-01T00:00:00Z' + }) + signed_stuff = { + 'auth_policies': ' '.join(policy_uris), + 'auth_time': '1970-01-01T00:00:00Z' + } + oid_req = DummySuccessResponse(openid_req_msg, signed_stuff) + req = pape.Response.fromSuccessResponse(oid_req) + self.failUnlessEqual(policy_uris, req.auth_policies) + self.failUnlessEqual('1970-01-01T00:00:00Z', req.auth_time) + + def test_fromSuccessResponseNoSignedArgs(self): + policy_uris = [pape.AUTH_MULTI_FACTOR, pape.AUTH_PHISHING_RESISTANT] + openid_req_msg = Message.fromOpenIDArgs({ + 'mode': 'id_res', + 'ns': OPENID2_NS, + 'ns.pape': pape.ns_uri, + 'pape.auth_policies': ' '.join(policy_uris), + 'pape.auth_time': '1970-01-01T00:00:00Z' + }) + + signed_stuff = {} + + class NoSigningDummyResponse(DummySuccessResponse): + def getSignedNS(self, ns_uri): + return None + + oid_req = NoSigningDummyResponse(openid_req_msg, signed_stuff) + resp = pape.Response.fromSuccessResponse(oid_req) + self.failUnless(resp is None) + +if __name__ == '__main__': + unittest.main() diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_parsehtml.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_parsehtml.py new file mode 100644 index 0000000..2f962cd --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_parsehtml.py @@ -0,0 +1,82 @@ +from openid.yadis.parsehtml import YadisHTMLParser, ParseDone +from HTMLParser import HTMLParseError + +import os.path, unittest, sys + +class _TestCase(unittest.TestCase): + reserved_values = ['None', 'EOF'] + + def __init__(self, filename, testname, expected, case): + self.filename = filename + self.testname = testname + self.expected = expected + self.case = case + unittest.TestCase.__init__(self) + + def runTest(self): + p = YadisHTMLParser() + try: + p.feed(self.case) + except ParseDone, why: + found = why[0] + + # make sure we protect outselves against accidental bogus + # test cases + assert found not in self.reserved_values + + # convert to a string + if found is None: + found = 'None' + + msg = "%r != %r for case %s" % (found, self.expected, self.case) + self.failUnlessEqual(found, self.expected, msg) + except HTMLParseError: + self.failUnless(self.expected == 'None', (self.case, self.expected)) + else: + self.failUnless(self.expected == 'EOF', (self.case, self.expected)) + + def shortDescription(self): + return "%s (%s<%s>)" % ( + self.testname, + self.__class__.__module__, + os.path.basename(self.filename)) + +def parseCases(data): + cases = [] + for chunk in data.split('\f\n'): + expected, case = chunk.split('\n', 1) + cases.append((expected, case)) + return cases + +def pyUnitTests(): + """Make a pyunit TestSuite from a file defining test cases.""" + s = unittest.TestSuite() + for (filename, test_num, expected, case) in getCases(): + s.addTest(_TestCase(filename, str(test_num), expected, case)) + return s + +def test(): + runner = unittest.TextTestRunner() + return runner.run(loadTests()) + +filenames = ['data/test1-parsehtml.txt'] + +default_test_files = [] +base = os.path.dirname(__file__) +for filename in filenames: + full_name = os.path.join(base, filename) + default_test_files.append(full_name) + +def getCases(test_files=default_test_files): + cases = [] + for filename in test_files: + test_num = 0 + data = file(filename).read() + for expected, case in parseCases(data): + test_num += 1 + cases.append((filename, test_num, expected, case)) + return cases + + +if __name__ == '__main__': + sys.exit(not test().wasSuccessful()) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_rpverify.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_rpverify.py new file mode 100644 index 0000000..9d781bb --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_rpverify.py @@ -0,0 +1,246 @@ +"""Unit tests for verification of return_to URLs for a realm +""" + +__all__ = ['TestBuildDiscoveryURL'] + +from openid.yadis.discover import DiscoveryResult, DiscoveryFailure +from openid.yadis import services +from openid.server import trustroot +from openid.test.support import CatchLogs +import unittest + +# Too many methods does not apply to unit test objects +#pylint:disable-msg=R0904 +class TestBuildDiscoveryURL(unittest.TestCase): + """Tests for building the discovery URL from a realm and a + return_to URL + """ + + def failUnlessDiscoURL(self, realm, expected_discovery_url): + """Build a discovery URL out of the realm and a return_to and + make sure that it matches the expected discovery URL + """ + realm_obj = trustroot.TrustRoot.parse(realm) + actual_discovery_url = realm_obj.buildDiscoveryURL() + self.failUnlessEqual(expected_discovery_url, actual_discovery_url) + + def test_trivial(self): + """There is no wildcard and the realm is the same as the return_to URL + """ + self.failUnlessDiscoURL('http://example.com/foo', + 'http://example.com/foo') + + def test_wildcard(self): + """There is a wildcard + """ + self.failUnlessDiscoURL('http://*.example.com/foo', + 'http://www.example.com/foo') + +class TestExtractReturnToURLs(unittest.TestCase): + disco_url = 'http://example.com/' + + def setUp(self): + self.original_discover = services.discover + services.discover = self.mockDiscover + self.data = None + + def tearDown(self): + services.discover = self.original_discover + + def mockDiscover(self, uri): + result = DiscoveryResult(uri) + result.response_text = self.data + result.normalized_uri = uri + return result + + def failUnlessFileHasReturnURLs(self, filename, expected_return_urls): + self.failUnlessXRDSHasReturnURLs(file(filename).read(), + expected_return_urls) + + def failUnlessXRDSHasReturnURLs(self, data, expected_return_urls): + self.data = data + actual_return_urls = list(trustroot.getAllowedReturnURLs( + self.disco_url)) + + self.failUnlessEqual(expected_return_urls, actual_return_urls) + + def failUnlessDiscoveryFailure(self, text): + self.data = text + self.failUnlessRaises( + DiscoveryFailure, trustroot.getAllowedReturnURLs, self.disco_url) + + def test_empty(self): + self.failUnlessDiscoveryFailure('') + + def test_badXML(self): + self.failUnlessDiscoveryFailure('>') + + def test_noEntries(self): + self.failUnlessXRDSHasReturnURLs('''\ + + + + + +''', []) + + def test_noReturnToEntries(self): + self.failUnlessXRDSHasReturnURLs('''\ + + + + + http://specs.openid.net/auth/2.0/server + http://www.myopenid.com/server + + + +''', []) + + def test_oneEntry(self): + self.failUnlessXRDSHasReturnURLs('''\ + + + + + http://specs.openid.net/auth/2.0/return_to + http://rp.example.com/return + + + +''', ['http://rp.example.com/return']) + + def test_twoEntries(self): + self.failUnlessXRDSHasReturnURLs('''\ + + + + + http://specs.openid.net/auth/2.0/return_to + http://rp.example.com/return + + + http://specs.openid.net/auth/2.0/return_to + http://other.rp.example.com/return + + + +''', ['http://rp.example.com/return', + 'http://other.rp.example.com/return']) + + def test_twoEntries_withOther(self): + self.failUnlessXRDSHasReturnURLs('''\ + + + + + http://specs.openid.net/auth/2.0/return_to + http://rp.example.com/return + + + http://specs.openid.net/auth/2.0/return_to + http://other.rp.example.com/return + + + http://example.com/LOLCATS + http://example.com/invisible+uri + + + +''', ['http://rp.example.com/return', + 'http://other.rp.example.com/return']) + + + +class TestReturnToMatches(unittest.TestCase): + def test_noEntries(self): + self.failIf(trustroot.returnToMatches([], 'anything')) + + def test_exactMatch(self): + r = 'http://example.com/return.to' + self.failUnless(trustroot.returnToMatches([r], r)) + + def test_garbageMatch(self): + r = 'http://example.com/return.to' + self.failUnless(trustroot.returnToMatches( + ['This is not a URL at all. In fact, it has characters, ' + 'like "<" that are not allowed in URLs', + r], + r)) + + def test_descendant(self): + r = 'http://example.com/return.to' + self.failUnless(trustroot.returnToMatches( + [r], + 'http://example.com/return.to/user:joe')) + + def test_wildcard(self): + self.failIf(trustroot.returnToMatches( + ['http://*.example.com/return.to'], + 'http://example.com/return.to')) + + def test_noMatch(self): + r = 'http://example.com/return.to' + self.failIf(trustroot.returnToMatches( + [r], + 'http://example.com/xss_exploit')) + +class TestVerifyReturnTo(unittest.TestCase, CatchLogs): + + def setUp(self): + CatchLogs.setUp(self) + + def tearDown(self): + CatchLogs.tearDown(self) + + def test_bogusRealm(self): + self.failIf(trustroot.verifyReturnTo('', 'http://example.com/')) + + def test_verifyWithDiscoveryCalled(self): + realm = 'http://*.example.com/' + return_to = 'http://www.example.com/foo' + + def vrfy(disco_url): + self.failUnlessEqual('http://www.example.com/', disco_url) + return [return_to] + + self.failUnless( + trustroot.verifyReturnTo(realm, return_to, _vrfy=vrfy)) + self.failUnlessLogEmpty() + + def test_verifyFailWithDiscoveryCalled(self): + realm = 'http://*.example.com/' + return_to = 'http://www.example.com/foo' + + def vrfy(disco_url): + self.failUnlessEqual('http://www.example.com/', disco_url) + return ['http://something-else.invalid/'] + + self.failIf( + trustroot.verifyReturnTo(realm, return_to, _vrfy=vrfy)) + self.failUnlessLogMatches("Failed to validate return_to") + + def test_verifyFailIfDiscoveryRedirects(self): + realm = 'http://*.example.com/' + return_to = 'http://www.example.com/foo' + + def vrfy(disco_url): + raise trustroot.RealmVerificationRedirected( + disco_url, "http://redirected.invalid") + + self.failIf( + trustroot.verifyReturnTo(realm, return_to, _vrfy=vrfy)) + self.failUnlessLogMatches("Attempting to verify") + +if __name__ == '__main__': + unittest.main() diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_server.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_server.py new file mode 100644 index 0000000..83261c8 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_server.py @@ -0,0 +1,2064 @@ +"""Tests for openid.server. +""" +from openid.server import server +from openid import association, cryptutil, oidutil +from openid.message import Message, OPENID_NS, OPENID2_NS, OPENID1_NS, \ + IDENTIFIER_SELECT, no_default, OPENID1_URL_LIMIT +from openid.store import memstore +import cgi + +import unittest +import warnings + +from urlparse import urlparse + +# In general, if you edit or add tests here, try to move in the direction +# of testing smaller units. For testing the external interfaces, we'll be +# developing an implementation-agnostic testing suite. + +# for more, see /etc/ssh/moduli + +ALT_MODULUS = 0xCAADDDEC1667FC68B5FA15D53C4E1532DD24561A1A2D47A12C01ABEA1E00731F6921AAC40742311FDF9E634BB7131BEE1AF240261554389A910425E044E88C8359B010F5AD2B80E29CB1A5B027B19D9E01A6F63A6F45E5D7ED2FF6A2A0085050A7D0CF307C3DB51D2490355907B4427C23A98DF1EB8ABEF2BA209BB7AFFE86A7 +ALT_GEN = 5 + +class CatchLogs(object): + def setUp(self): + self.old_logger = oidutil.log + oidutil.log = self.gotLogMessage + self.messages = [] + + def gotLogMessage(self, message): + self.messages.append(message) + + def tearDown(self): + oidutil.log = self.old_logger + +class TestProtocolError(unittest.TestCase): + def test_browserWithReturnTo(self): + return_to = "http://rp.unittest/consumer" + # will be a ProtocolError raised by Decode or CheckIDRequest.answer + args = Message.fromPostArgs({ + 'openid.mode': 'monkeydance', + 'openid.identity': 'http://wagu.unittest/', + 'openid.return_to': return_to, + }) + e = server.ProtocolError(args, "plucky") + self.failUnless(e.hasReturnTo()) + expected_args = { + 'openid.mode': ['error'], + 'openid.error': ['plucky'], + } + + rt_base, result_args = e.encodeToURL().split('?', 1) + result_args = cgi.parse_qs(result_args) + self.failUnlessEqual(result_args, expected_args) + + def test_browserWithReturnTo_OpenID2_GET(self): + return_to = "http://rp.unittest/consumer" + # will be a ProtocolError raised by Decode or CheckIDRequest.answer + args = Message.fromPostArgs({ + 'openid.ns': OPENID2_NS, + 'openid.mode': 'monkeydance', + 'openid.identity': 'http://wagu.unittest/', + 'openid.claimed_id': 'http://wagu.unittest/', + 'openid.return_to': return_to, + }) + e = server.ProtocolError(args, "plucky") + self.failUnless(e.hasReturnTo()) + expected_args = { + 'openid.ns': [OPENID2_NS], + 'openid.mode': ['error'], + 'openid.error': ['plucky'], + } + + rt_base, result_args = e.encodeToURL().split('?', 1) + result_args = cgi.parse_qs(result_args) + self.failUnlessEqual(result_args, expected_args) + + def test_browserWithReturnTo_OpenID2_POST(self): + return_to = "http://rp.unittest/consumer" + ('x' * OPENID1_URL_LIMIT) + # will be a ProtocolError raised by Decode or CheckIDRequest.answer + args = Message.fromPostArgs({ + 'openid.ns': OPENID2_NS, + 'openid.mode': 'monkeydance', + 'openid.identity': 'http://wagu.unittest/', + 'openid.claimed_id': 'http://wagu.unittest/', + 'openid.return_to': return_to, + }) + e = server.ProtocolError(args, "plucky") + self.failUnless(e.hasReturnTo()) + expected_args = { + 'openid.ns': [OPENID2_NS], + 'openid.mode': ['error'], + 'openid.error': ['plucky'], + } + + self.failUnless(e.whichEncoding() == server.ENCODE_HTML_FORM) + self.failUnless(e.toFormMarkup() == e.toMessage().toFormMarkup( + args.getArg(OPENID_NS, 'return_to'))) + + def test_browserWithReturnTo_OpenID1_exceeds_limit(self): + return_to = "http://rp.unittest/consumer" + ('x' * OPENID1_URL_LIMIT) + # will be a ProtocolError raised by Decode or CheckIDRequest.answer + args = Message.fromPostArgs({ + 'openid.mode': 'monkeydance', + 'openid.identity': 'http://wagu.unittest/', + 'openid.return_to': return_to, + }) + e = server.ProtocolError(args, "plucky") + self.failUnless(e.hasReturnTo()) + expected_args = { + 'openid.mode': ['error'], + 'openid.error': ['plucky'], + } + + self.failUnless(e.whichEncoding() == server.ENCODE_URL) + + rt_base, result_args = e.encodeToURL().split('?', 1) + result_args = cgi.parse_qs(result_args) + self.failUnlessEqual(result_args, expected_args) + + def test_noReturnTo(self): + # will be a ProtocolError raised by Decode or CheckIDRequest.answer + args = Message.fromPostArgs({ + 'openid.mode': 'zebradance', + 'openid.identity': 'http://wagu.unittest/', + }) + e = server.ProtocolError(args, "waffles") + self.failIf(e.hasReturnTo()) + expected = """error:waffles +mode:error +""" + self.failUnlessEqual(e.encodeToKVForm(), expected) + + + def test_noMessage(self): + e = server.ProtocolError(None, "no moar pancakes") + self.failIf(e.hasReturnTo()) + self.failUnlessEqual(e.whichEncoding(), None) + + +class TestDecode(unittest.TestCase): + def setUp(self): + self.claimed_id = 'http://de.legating.de.coder.unittest/' + self.id_url = "http://decoder.am.unittest/" + self.rt_url = "http://rp.unittest/foobot/?qux=zam" + self.tr_url = "http://rp.unittest/" + self.assoc_handle = "{assoc}{handle}" + self.op_endpoint = 'http://endpoint.unittest/encode' + self.store = memstore.MemoryStore() + self.server = server.Server(self.store, self.op_endpoint) + self.decode = self.server.decoder.decode + self.decode = server.Decoder(self.server).decode + + def test_none(self): + args = {} + r = self.decode(args) + self.failUnlessEqual(r, None) + + def test_irrelevant(self): + args = { + 'pony': 'spotted', + 'sreg.mutant_power': 'decaffinator', + } + self.failUnlessRaises(server.ProtocolError, self.decode, args) + + def test_bad(self): + args = { + 'openid.mode': 'twos-compliment', + 'openid.pants': 'zippered', + } + self.failUnlessRaises(server.ProtocolError, self.decode, args) + + def test_dictOfLists(self): + args = { + 'openid.mode': ['checkid_setup'], + 'openid.identity': self.id_url, + 'openid.assoc_handle': self.assoc_handle, + 'openid.return_to': self.rt_url, + 'openid.trust_root': self.tr_url, + } + try: + result = self.decode(args) + except TypeError, err: + self.failUnless(str(err).find('values') != -1, err) + else: + self.fail("Expected TypeError, but got result %s" % (result,)) + + def test_checkidImmediate(self): + args = { + 'openid.mode': 'checkid_immediate', + 'openid.identity': self.id_url, + 'openid.assoc_handle': self.assoc_handle, + 'openid.return_to': self.rt_url, + 'openid.trust_root': self.tr_url, + # should be ignored + 'openid.some.extension': 'junk', + } + r = self.decode(args) + self.failUnless(isinstance(r, server.CheckIDRequest)) + self.failUnlessEqual(r.mode, "checkid_immediate") + self.failUnlessEqual(r.immediate, True) + self.failUnlessEqual(r.identity, self.id_url) + self.failUnlessEqual(r.trust_root, self.tr_url) + self.failUnlessEqual(r.return_to, self.rt_url) + self.failUnlessEqual(r.assoc_handle, self.assoc_handle) + + def test_checkidSetup(self): + args = { + 'openid.mode': 'checkid_setup', + 'openid.identity': self.id_url, + 'openid.assoc_handle': self.assoc_handle, + 'openid.return_to': self.rt_url, + 'openid.trust_root': self.tr_url, + } + r = self.decode(args) + self.failUnless(isinstance(r, server.CheckIDRequest)) + self.failUnlessEqual(r.mode, "checkid_setup") + self.failUnlessEqual(r.immediate, False) + self.failUnlessEqual(r.identity, self.id_url) + self.failUnlessEqual(r.trust_root, self.tr_url) + self.failUnlessEqual(r.return_to, self.rt_url) + + def test_checkidSetupOpenID2(self): + args = { + 'openid.ns': OPENID2_NS, + 'openid.mode': 'checkid_setup', + 'openid.identity': self.id_url, + 'openid.claimed_id': self.claimed_id, + 'openid.assoc_handle': self.assoc_handle, + 'openid.return_to': self.rt_url, + 'openid.realm': self.tr_url, + } + r = self.decode(args) + self.failUnless(isinstance(r, server.CheckIDRequest)) + self.failUnlessEqual(r.mode, "checkid_setup") + self.failUnlessEqual(r.immediate, False) + self.failUnlessEqual(r.identity, self.id_url) + self.failUnlessEqual(r.claimed_id, self.claimed_id) + self.failUnlessEqual(r.trust_root, self.tr_url) + self.failUnlessEqual(r.return_to, self.rt_url) + + def test_checkidSetupNoClaimedIDOpenID2(self): + args = { + 'openid.ns': OPENID2_NS, + 'openid.mode': 'checkid_setup', + 'openid.identity': self.id_url, + 'openid.assoc_handle': self.assoc_handle, + 'openid.return_to': self.rt_url, + 'openid.realm': self.tr_url, + } + self.failUnlessRaises(server.ProtocolError, self.decode, args) + + def test_checkidSetupNoIdentityOpenID2(self): + args = { + 'openid.ns': OPENID2_NS, + 'openid.mode': 'checkid_setup', + 'openid.assoc_handle': self.assoc_handle, + 'openid.return_to': self.rt_url, + 'openid.realm': self.tr_url, + } + r = self.decode(args) + self.failUnless(isinstance(r, server.CheckIDRequest)) + self.failUnlessEqual(r.mode, "checkid_setup") + self.failUnlessEqual(r.immediate, False) + self.failUnlessEqual(r.identity, None) + self.failUnlessEqual(r.trust_root, self.tr_url) + self.failUnlessEqual(r.return_to, self.rt_url) + + def test_checkidSetupNoReturnOpenID1(self): + """Make sure an OpenID 1 request cannot be decoded if it lacks + a return_to. + """ + args = { + 'openid.mode': 'checkid_setup', + 'openid.identity': self.id_url, + 'openid.assoc_handle': self.assoc_handle, + 'openid.trust_root': self.tr_url, + } + self.failUnlessRaises(server.ProtocolError, self.decode, args) + + def test_checkidSetupNoReturnOpenID2(self): + """Make sure an OpenID 2 request with no return_to can be + decoded, and make sure a response to such a request raises + NoReturnToError. + """ + args = { + 'openid.ns': OPENID2_NS, + 'openid.mode': 'checkid_setup', + 'openid.identity': self.id_url, + 'openid.claimed_id': self.id_url, + 'openid.assoc_handle': self.assoc_handle, + 'openid.realm': self.tr_url, + } + self.failUnless(isinstance(self.decode(args), server.CheckIDRequest)) + + req = self.decode(args) + self.assertRaises(server.NoReturnToError, req.answer, False) + self.assertRaises(server.NoReturnToError, req.encodeToURL, 'bogus') + self.assertRaises(server.NoReturnToError, req.getCancelURL) + + def test_checkidSetupRealmRequiredOpenID2(self): + """Make sure that an OpenID 2 request which lacks return_to + cannot be decoded if it lacks a realm. Spec: This value + (openid.realm) MUST be sent if openid.return_to is omitted. + """ + args = { + 'openid.ns': OPENID2_NS, + 'openid.mode': 'checkid_setup', + 'openid.identity': self.id_url, + 'openid.assoc_handle': self.assoc_handle, + } + self.failUnlessRaises(server.ProtocolError, self.decode, args) + + def test_checkidSetupBadReturn(self): + args = { + 'openid.mode': 'checkid_setup', + 'openid.identity': self.id_url, + 'openid.assoc_handle': self.assoc_handle, + 'openid.return_to': 'not a url', + } + try: + result = self.decode(args) + except server.ProtocolError, err: + self.failUnless(err.openid_message) + else: + self.fail("Expected ProtocolError, instead returned with %s" % + (result,)) + + def test_checkidSetupUntrustedReturn(self): + args = { + 'openid.mode': 'checkid_setup', + 'openid.identity': self.id_url, + 'openid.assoc_handle': self.assoc_handle, + 'openid.return_to': self.rt_url, + 'openid.trust_root': 'http://not-the-return-place.unittest/', + } + try: + result = self.decode(args) + except server.UntrustedReturnURL, err: + self.failUnless(err.openid_message) + else: + self.fail("Expected UntrustedReturnURL, instead returned with %s" % + (result,)) + + def test_checkAuth(self): + args = { + 'openid.mode': 'check_authentication', + 'openid.assoc_handle': '{dumb}{handle}', + 'openid.sig': 'sigblob', + 'openid.signed': 'identity,return_to,response_nonce,mode', + 'openid.identity': 'signedval1', + 'openid.return_to': 'signedval2', + 'openid.response_nonce': 'signedval3', + 'openid.baz': 'unsigned', + } + r = self.decode(args) + self.failUnless(isinstance(r, server.CheckAuthRequest)) + self.failUnlessEqual(r.mode, 'check_authentication') + self.failUnlessEqual(r.sig, 'sigblob') + + + def test_checkAuthMissingSignature(self): + args = { + 'openid.mode': 'check_authentication', + 'openid.assoc_handle': '{dumb}{handle}', + 'openid.signed': 'foo,bar,mode', + 'openid.foo': 'signedval1', + 'openid.bar': 'signedval2', + 'openid.baz': 'unsigned', + } + self.failUnlessRaises(server.ProtocolError, self.decode, args) + + + def test_checkAuthAndInvalidate(self): + args = { + 'openid.mode': 'check_authentication', + 'openid.assoc_handle': '{dumb}{handle}', + 'openid.invalidate_handle': '[[SMART_handle]]', + 'openid.sig': 'sigblob', + 'openid.signed': 'identity,return_to,response_nonce,mode', + 'openid.identity': 'signedval1', + 'openid.return_to': 'signedval2', + 'openid.response_nonce': 'signedval3', + 'openid.baz': 'unsigned', + } + r = self.decode(args) + self.failUnless(isinstance(r, server.CheckAuthRequest)) + self.failUnlessEqual(r.invalidate_handle, '[[SMART_handle]]') + + + def test_associateDH(self): + args = { + 'openid.mode': 'associate', + 'openid.session_type': 'DH-SHA1', + 'openid.dh_consumer_public': "Rzup9265tw==", + } + r = self.decode(args) + self.failUnless(isinstance(r, server.AssociateRequest)) + self.failUnlessEqual(r.mode, "associate") + self.failUnlessEqual(r.session.session_type, "DH-SHA1") + self.failUnlessEqual(r.assoc_type, "HMAC-SHA1") + self.failUnless(r.session.consumer_pubkey) + + def test_associateDHMissingKey(self): + """Trying DH assoc w/o public key""" + args = { + 'openid.mode': 'associate', + 'openid.session_type': 'DH-SHA1', + } + # Using DH-SHA1 without supplying dh_consumer_public is an error. + self.failUnlessRaises(server.ProtocolError, self.decode, args) + + + def test_associateDHpubKeyNotB64(self): + args = { + 'openid.mode': 'associate', + 'openid.session_type': 'DH-SHA1', + 'openid.dh_consumer_public': "donkeydonkeydonkey", + } + self.failUnlessRaises(server.ProtocolError, self.decode, args) + + + def test_associateDHModGen(self): + # test dh with non-default but valid values for dh_modulus and dh_gen + args = { + 'openid.mode': 'associate', + 'openid.session_type': 'DH-SHA1', + 'openid.dh_consumer_public': "Rzup9265tw==", + 'openid.dh_modulus': cryptutil.longToBase64(ALT_MODULUS), + 'openid.dh_gen': cryptutil.longToBase64(ALT_GEN) , + } + r = self.decode(args) + self.failUnless(isinstance(r, server.AssociateRequest)) + self.failUnlessEqual(r.mode, "associate") + self.failUnlessEqual(r.session.session_type, "DH-SHA1") + self.failUnlessEqual(r.assoc_type, "HMAC-SHA1") + self.failUnlessEqual(r.session.dh.modulus, ALT_MODULUS) + self.failUnlessEqual(r.session.dh.generator, ALT_GEN) + self.failUnless(r.session.consumer_pubkey) + + + def test_associateDHCorruptModGen(self): + # test dh with non-default but valid values for dh_modulus and dh_gen + args = { + 'openid.mode': 'associate', + 'openid.session_type': 'DH-SHA1', + 'openid.dh_consumer_public': "Rzup9265tw==", + 'openid.dh_modulus': 'pizza', + 'openid.dh_gen': 'gnocchi', + } + self.failUnlessRaises(server.ProtocolError, self.decode, args) + + + def test_associateDHMissingModGen(self): + # test dh with non-default but valid values for dh_modulus and dh_gen + args = { + 'openid.mode': 'associate', + 'openid.session_type': 'DH-SHA1', + 'openid.dh_consumer_public': "Rzup9265tw==", + 'openid.dh_modulus': 'pizza', + } + self.failUnlessRaises(server.ProtocolError, self.decode, args) + + +# def test_associateDHInvalidModGen(self): +# # test dh with properly encoded values that are not a valid +# # modulus/generator combination. +# args = { +# 'openid.mode': 'associate', +# 'openid.session_type': 'DH-SHA1', +# 'openid.dh_consumer_public': "Rzup9265tw==", +# 'openid.dh_modulus': cryptutil.longToBase64(9), +# 'openid.dh_gen': cryptutil.longToBase64(27) , +# } +# self.failUnlessRaises(server.ProtocolError, self.decode, args) +# test_associateDHInvalidModGen.todo = "low-priority feature" + + + def test_associateWeirdSession(self): + args = { + 'openid.mode': 'associate', + 'openid.session_type': 'FLCL6', + 'openid.dh_consumer_public': "YQ==\n", + } + self.failUnlessRaises(server.ProtocolError, self.decode, args) + + + def test_associatePlain(self): + args = { + 'openid.mode': 'associate', + } + r = self.decode(args) + self.failUnless(isinstance(r, server.AssociateRequest)) + self.failUnlessEqual(r.mode, "associate") + self.failUnlessEqual(r.session.session_type, "no-encryption") + self.failUnlessEqual(r.assoc_type, "HMAC-SHA1") + + def test_nomode(self): + args = { + 'openid.session_type': 'DH-SHA1', + 'openid.dh_consumer_public': "my public keeey", + } + self.failUnlessRaises(server.ProtocolError, self.decode, args) + + def test_invalidns(self): + args = {'openid.ns': 'Tuesday', + 'openid.mode': 'associate'} + + try: + r = self.decode(args) + except server.ProtocolError, err: + # Assert that the ProtocolError does have a Message attached + # to it, even though the request wasn't a well-formed Message. + self.failUnless(err.openid_message) + # The error message contains the bad openid.ns. + self.failUnless('Tuesday' in str(err), str(err)) + else: + self.fail("Expected ProtocolError but returned with %r" % (r,)) + + +class TestEncode(unittest.TestCase): + def setUp(self): + self.encoder = server.Encoder() + self.encode = self.encoder.encode + self.op_endpoint = 'http://endpoint.unittest/encode' + self.store = memstore.MemoryStore() + self.server = server.Server(self.store, self.op_endpoint) + + def test_id_res_OpenID2_GET(self): + """ + Check that when an OpenID 2 response does not exceed the + OpenID 1 message size, a GET response (i.e., redirect) is + issued. + """ + request = server.CheckIDRequest( + identity = 'http://bombom.unittest/', + trust_root = 'http://burr.unittest/', + return_to = 'http://burr.unittest/999', + immediate = False, + op_endpoint = self.server.op_endpoint, + ) + request.message = Message(OPENID2_NS) + response = server.OpenIDResponse(request) + response.fields = Message.fromOpenIDArgs({ + 'ns': OPENID2_NS, + 'mode': 'id_res', + 'identity': request.identity, + 'claimed_id': request.identity, + 'return_to': request.return_to, + }) + + self.failIf(response.renderAsForm()) + self.failUnless(response.whichEncoding() == server.ENCODE_URL) + webresponse = self.encode(response) + self.failUnless(webresponse.headers.has_key('location')) + + def test_id_res_OpenID2_POST(self): + """ + Check that when an OpenID 2 response exceeds the OpenID 1 + message size, a POST response (i.e., an HTML form) is + returned. + """ + request = server.CheckIDRequest( + identity = 'http://bombom.unittest/', + trust_root = 'http://burr.unittest/', + return_to = 'http://burr.unittest/999', + immediate = False, + op_endpoint = self.server.op_endpoint, + ) + request.message = Message(OPENID2_NS) + response = server.OpenIDResponse(request) + response.fields = Message.fromOpenIDArgs({ + 'ns': OPENID2_NS, + 'mode': 'id_res', + 'identity': request.identity, + 'claimed_id': request.identity, + 'return_to': 'x' * OPENID1_URL_LIMIT, + }) + + self.failUnless(response.renderAsForm()) + self.failUnless(len(response.encodeToURL()) > OPENID1_URL_LIMIT) + self.failUnless(response.whichEncoding() == server.ENCODE_HTML_FORM) + webresponse = self.encode(response) + self.failUnlessEqual(webresponse.body, response.toFormMarkup()) + + def test_toFormMarkup(self): + request = server.CheckIDRequest( + identity = 'http://bombom.unittest/', + trust_root = 'http://burr.unittest/', + return_to = 'http://burr.unittest/999', + immediate = False, + op_endpoint = self.server.op_endpoint, + ) + request.message = Message(OPENID2_NS) + response = server.OpenIDResponse(request) + response.fields = Message.fromOpenIDArgs({ + 'ns': OPENID2_NS, + 'mode': 'id_res', + 'identity': request.identity, + 'claimed_id': request.identity, + 'return_to': 'x' * OPENID1_URL_LIMIT, + }) + + form_markup = response.toFormMarkup({'foo':'bar'}) + self.failUnless(' foo="bar"' in form_markup) + + def test_toHTML(self): + request = server.CheckIDRequest( + identity = 'http://bombom.unittest/', + trust_root = 'http://burr.unittest/', + return_to = 'http://burr.unittest/999', + immediate = False, + op_endpoint = self.server.op_endpoint, + ) + request.message = Message(OPENID2_NS) + response = server.OpenIDResponse(request) + response.fields = Message.fromOpenIDArgs({ + 'ns': OPENID2_NS, + 'mode': 'id_res', + 'identity': request.identity, + 'claimed_id': request.identity, + 'return_to': 'x' * OPENID1_URL_LIMIT, + }) + html = response.toHTML() + self.failUnless('' in html) + self.failUnless('' in html) + self.failUnless(' OPENID1_URL_LIMIT) + self.failUnless(response.whichEncoding() == server.ENCODE_URL) + webresponse = self.encode(response) + self.failUnlessEqual(webresponse.headers['location'], response.encodeToURL()) + + def test_id_res(self): + request = server.CheckIDRequest( + identity = 'http://bombom.unittest/', + trust_root = 'http://burr.unittest/', + return_to = 'http://burr.unittest/999', + immediate = False, + op_endpoint = self.server.op_endpoint, + ) + request.message = Message(OPENID2_NS) + response = server.OpenIDResponse(request) + response.fields = Message.fromOpenIDArgs({ + 'mode': 'id_res', + 'identity': request.identity, + 'return_to': request.return_to, + }) + webresponse = self.encode(response) + self.failUnlessEqual(webresponse.code, server.HTTP_REDIRECT) + self.failUnless(webresponse.headers.has_key('location')) + + location = webresponse.headers['location'] + self.failUnless(location.startswith(request.return_to), + "%s does not start with %s" % (location, + request.return_to)) + # argh. + q2 = dict(cgi.parse_qsl(urlparse(location)[4])) + expected = response.fields.toPostArgs() + self.failUnlessEqual(q2, expected) + + def test_cancel(self): + request = server.CheckIDRequest( + identity = 'http://bombom.unittest/', + trust_root = 'http://burr.unittest/', + return_to = 'http://burr.unittest/999', + immediate = False, + op_endpoint = self.server.op_endpoint, + ) + request.message = Message(OPENID2_NS) + response = server.OpenIDResponse(request) + response.fields = Message.fromOpenIDArgs({ + 'mode': 'cancel', + }) + webresponse = self.encode(response) + self.failUnlessEqual(webresponse.code, server.HTTP_REDIRECT) + self.failUnless(webresponse.headers.has_key('location')) + + def test_cancelToForm(self): + request = server.CheckIDRequest( + identity = 'http://bombom.unittest/', + trust_root = 'http://burr.unittest/', + return_to = 'http://burr.unittest/999', + immediate = False, + op_endpoint = self.server.op_endpoint, + ) + request.message = Message(OPENID2_NS) + response = server.OpenIDResponse(request) + response.fields = Message.fromOpenIDArgs({ + 'mode': 'cancel', + }) + form = response.toFormMarkup() + self.failUnless(form) + + def test_assocReply(self): + msg = Message(OPENID2_NS) + msg.setArg(OPENID2_NS, 'session_type', 'no-encryption') + request = server.AssociateRequest.fromMessage(msg) + response = server.OpenIDResponse(request) + response.fields = Message.fromPostArgs( + {'openid.assoc_handle': "every-zig"}) + webresponse = self.encode(response) + body = """assoc_handle:every-zig +""" + self.failUnlessEqual(webresponse.code, server.HTTP_OK) + self.failUnlessEqual(webresponse.headers, {}) + self.failUnlessEqual(webresponse.body, body) + + def test_checkauthReply(self): + request = server.CheckAuthRequest('a_sock_monkey', + 'siggggg', + []) + response = server.OpenIDResponse(request) + response.fields = Message.fromOpenIDArgs({ + 'is_valid': 'true', + 'invalidate_handle': 'xXxX:xXXx' + }) + body = """invalidate_handle:xXxX:xXXx +is_valid:true +""" + webresponse = self.encode(response) + self.failUnlessEqual(webresponse.code, server.HTTP_OK) + self.failUnlessEqual(webresponse.headers, {}) + self.failUnlessEqual(webresponse.body, body) + + def test_unencodableError(self): + args = Message.fromPostArgs({ + 'openid.identity': 'http://limu.unittest/', + }) + e = server.ProtocolError(args, "wet paint") + self.failUnlessRaises(server.EncodingError, self.encode, e) + + def test_encodableError(self): + args = Message.fromPostArgs({ + 'openid.mode': 'associate', + 'openid.identity': 'http://limu.unittest/', + }) + body="error:snoot\nmode:error\n" + webresponse = self.encode(server.ProtocolError(args, "snoot")) + self.failUnlessEqual(webresponse.code, server.HTTP_ERROR) + self.failUnlessEqual(webresponse.headers, {}) + self.failUnlessEqual(webresponse.body, body) + + + +class TestSigningEncode(unittest.TestCase): + def setUp(self): + self._dumb_key = server.Signatory._dumb_key + self._normal_key = server.Signatory._normal_key + self.store = memstore.MemoryStore() + self.server = server.Server(self.store, "http://signing.unittest/enc") + self.request = server.CheckIDRequest( + identity = 'http://bombom.unittest/', + trust_root = 'http://burr.unittest/', + return_to = 'http://burr.unittest/999', + immediate = False, + op_endpoint = self.server.op_endpoint, + ) + self.request.message = Message(OPENID2_NS) + self.response = server.OpenIDResponse(self.request) + self.response.fields = Message.fromOpenIDArgs({ + 'mode': 'id_res', + 'identity': self.request.identity, + 'return_to': self.request.return_to, + }) + self.signatory = server.Signatory(self.store) + self.encoder = server.SigningEncoder(self.signatory) + self.encode = self.encoder.encode + + def test_idres(self): + assoc_handle = '{bicycle}{shed}' + self.store.storeAssociation( + self._normal_key, + association.Association.fromExpiresIn(60, assoc_handle, + 'sekrit', 'HMAC-SHA1')) + self.request.assoc_handle = assoc_handle + webresponse = self.encode(self.response) + self.failUnlessEqual(webresponse.code, server.HTTP_REDIRECT) + self.failUnless(webresponse.headers.has_key('location')) + + location = webresponse.headers['location'] + query = cgi.parse_qs(urlparse(location)[4]) + self.failUnless('openid.sig' in query) + self.failUnless('openid.assoc_handle' in query) + self.failUnless('openid.signed' in query) + + def test_idresDumb(self): + webresponse = self.encode(self.response) + self.failUnlessEqual(webresponse.code, server.HTTP_REDIRECT) + self.failUnless(webresponse.headers.has_key('location')) + + location = webresponse.headers['location'] + query = cgi.parse_qs(urlparse(location)[4]) + self.failUnless('openid.sig' in query) + self.failUnless('openid.assoc_handle' in query) + self.failUnless('openid.signed' in query) + + def test_forgotStore(self): + self.encoder.signatory = None + self.failUnlessRaises(ValueError, self.encode, self.response) + + def test_cancel(self): + request = server.CheckIDRequest( + identity = 'http://bombom.unittest/', + trust_root = 'http://burr.unittest/', + return_to = 'http://burr.unittest/999', + immediate = False, + op_endpoint = self.server.op_endpoint, + ) + request.message = Message(OPENID2_NS) + response = server.OpenIDResponse(request) + response.fields.setArg(OPENID_NS, 'mode', 'cancel') + webresponse = self.encode(response) + self.failUnlessEqual(webresponse.code, server.HTTP_REDIRECT) + self.failUnless(webresponse.headers.has_key('location')) + location = webresponse.headers['location'] + query = cgi.parse_qs(urlparse(location)[4]) + self.failIf('openid.sig' in query, response.fields.toPostArgs()) + + def test_assocReply(self): + msg = Message(OPENID2_NS) + msg.setArg(OPENID2_NS, 'session_type', 'no-encryption') + request = server.AssociateRequest.fromMessage(msg) + response = server.OpenIDResponse(request) + response.fields = Message.fromOpenIDArgs({'assoc_handle': "every-zig"}) + webresponse = self.encode(response) + body = """assoc_handle:every-zig +""" + self.failUnlessEqual(webresponse.code, server.HTTP_OK) + self.failUnlessEqual(webresponse.headers, {}) + self.failUnlessEqual(webresponse.body, body) + + def test_alreadySigned(self): + self.response.fields.setArg(OPENID_NS, 'sig', 'priorSig==') + self.failUnlessRaises(server.AlreadySigned, self.encode, self.response) + +class TestCheckID(unittest.TestCase): + def setUp(self): + self.op_endpoint = 'http://endpoint.unittest/' + self.store = memstore.MemoryStore() + self.server = server.Server(self.store, self.op_endpoint) + self.request = server.CheckIDRequest( + identity = 'http://bambam.unittest/', + trust_root = 'http://bar.unittest/', + return_to = 'http://bar.unittest/999', + immediate = False, + op_endpoint = self.server.op_endpoint, + ) + self.request.message = Message(OPENID2_NS) + + def test_trustRootInvalid(self): + self.request.trust_root = "http://foo.unittest/17" + self.request.return_to = "http://foo.unittest/39" + self.failIf(self.request.trustRootValid()) + + def test_trustRootValid(self): + self.request.trust_root = "http://foo.unittest/" + self.request.return_to = "http://foo.unittest/39" + self.failUnless(self.request.trustRootValid()) + + def test_malformedTrustRoot(self): + self.request.trust_root = "invalid://trust*root/" + self.request.return_to = "http://foo.unittest/39" + sentinel = object() + self.request.message = sentinel + try: + result = self.request.trustRootValid() + except server.MalformedTrustRoot, why: + self.failUnless(sentinel is why.openid_message) + else: + self.fail('Expected MalformedTrustRoot exception. Got %r' + % (result,)) + + def test_trustRootValidNoReturnTo(self): + request = server.CheckIDRequest( + identity = 'http://bambam.unittest/', + trust_root = 'http://bar.unittest/', + return_to = None, + immediate = False, + op_endpoint = self.server.op_endpoint, + ) + + self.failUnless(request.trustRootValid()) + + def test_returnToVerified_callsVerify(self): + """Make sure that verifyReturnTo is calling the trustroot + function verifyReturnTo + """ + def withVerifyReturnTo(new_verify, callable): + old_verify = server.verifyReturnTo + try: + server.verifyReturnTo = new_verify + return callable() + finally: + server.verifyReturnTo = old_verify + + # Ensure that exceptions are passed through + sentinel = Exception() + def vrfyExc(trust_root, return_to): + self.failUnlessEqual(self.request.trust_root, trust_root) + self.failUnlessEqual(self.request.return_to, return_to) + raise sentinel + + try: + withVerifyReturnTo(vrfyExc, self.request.returnToVerified) + except Exception, e: + self.failUnless(e is sentinel, e) + + # Ensure that True and False are passed through unchanged + def constVerify(val): + def verify(trust_root, return_to): + self.failUnlessEqual(self.request.trust_root, trust_root) + self.failUnlessEqual(self.request.return_to, return_to) + return val + return verify + + for val in [True, False]: + self.failUnlessEqual( + val, + withVerifyReturnTo(constVerify(val), + self.request.returnToVerified)) + + def _expectAnswer(self, answer, identity=None, claimed_id=None): + expected_list = [ + ('mode', 'id_res'), + ('return_to', self.request.return_to), + ('op_endpoint', self.op_endpoint), + ] + if identity: + expected_list.append(('identity', identity)) + if claimed_id: + expected_list.append(('claimed_id', claimed_id)) + else: + expected_list.append(('claimed_id', identity)) + + for k, expected in expected_list: + actual = answer.fields.getArg(OPENID_NS, k) + self.failUnlessEqual(actual, expected, "%s: expected %s, got %s" % (k, expected, actual)) + + self.failUnless(answer.fields.hasKey(OPENID_NS, 'response_nonce')) + self.failUnless(answer.fields.getOpenIDNamespace() == OPENID2_NS) + + # One for nonce, one for ns + self.failUnlessEqual(len(answer.fields.toPostArgs()), + len(expected_list) + 2, + answer.fields.toPostArgs()) + + def test_answerAllow(self): + """Check the fields specified by "Positive Assertions" + + including mode=id_res, identity, claimed_id, op_endpoint, return_to + """ + answer = self.request.answer(True) + self.failUnlessEqual(answer.request, self.request) + self._expectAnswer(answer, self.request.identity) + + def test_answerAllowDelegatedIdentity(self): + self.request.claimed_id = 'http://delegating.unittest/' + answer = self.request.answer(True) + self._expectAnswer(answer, self.request.identity, + self.request.claimed_id) + + def test_answerAllowDelegatedIdentity2(self): + # This time with the identity argument explicitly passed in to + # answer() + self.request.claimed_id = 'http://delegating.unittest/' + answer = self.request.answer(True, identity='http://bambam.unittest/') + self._expectAnswer(answer, self.request.identity, + self.request.claimed_id) + + def test_answerAllowWithoutIdentityReally(self): + self.request.identity = None + answer = self.request.answer(True) + self.failUnlessEqual(answer.request, self.request) + self._expectAnswer(answer) + + def test_answerAllowAnonymousFail(self): + self.request.identity = None + # XXX - Check on this, I think this behavior is legal in OpenID 2.0? + self.failUnlessRaises( + ValueError, self.request.answer, True, identity="=V") + + def test_answerAllowWithIdentity(self): + self.request.identity = IDENTIFIER_SELECT + selected_id = 'http://anon.unittest/9861' + answer = self.request.answer(True, identity=selected_id) + self._expectAnswer(answer, selected_id) + + def test_answerAllowWithDelegatedIdentityOpenID2(self): + """Answer an IDENTIFIER_SELECT case with a delegated identifier. + """ + # claimed_id delegates to selected_id here. + self.request.identity = IDENTIFIER_SELECT + selected_id = 'http://anon.unittest/9861' + claimed_id = 'http://monkeyhat.unittest/' + answer = self.request.answer(True, identity=selected_id, + claimed_id=claimed_id) + self._expectAnswer(answer, selected_id, claimed_id) + + def test_answerAllowWithDelegatedIdentityOpenID1(self): + """claimed_id parameter doesn't exist in OpenID 1. + """ + self.request.message = Message(OPENID1_NS) + # claimed_id delegates to selected_id here. + self.request.identity = IDENTIFIER_SELECT + selected_id = 'http://anon.unittest/9861' + claimed_id = 'http://monkeyhat.unittest/' + self.failUnlessRaises(server.VersionError, + self.request.answer, True, + identity=selected_id, + claimed_id=claimed_id) + + def test_answerAllowWithAnotherIdentity(self): + # XXX - Check on this, I think this behavior is legal in OpenID 2.0? + self.failUnlessRaises(ValueError, self.request.answer, True, + identity="http://pebbles.unittest/") + + def test_answerAllowWithIdentityNormalization(self): + # The RP has sent us a non-normalized value for openid.identity, + # and the library user is passing an explicit value for identity + # to CheckIDRequest.answer. + non_normalized = 'http://bambam.unittest' + normalized = non_normalized + '/' + + self.request.identity = non_normalized + self.request.claimed_id = non_normalized + + answer = self.request.answer(True, identity=normalized) + + # Expect the values that were sent in the request, even though + # they're not normalized. + self._expectAnswer(answer, identity=non_normalized, + claimed_id=non_normalized) + + def test_answerAllowNoIdentityOpenID1(self): + self.request.message = Message(OPENID1_NS) + self.request.identity = None + self.failUnlessRaises(ValueError, self.request.answer, True, + identity=None) + + def test_answerAllowForgotEndpoint(self): + self.request.op_endpoint = None + self.failUnlessRaises(RuntimeError, self.request.answer, True) + + def test_checkIDWithNoIdentityOpenID1(self): + msg = Message(OPENID1_NS) + msg.setArg(OPENID_NS, 'return_to', 'bogus') + msg.setArg(OPENID_NS, 'trust_root', 'bogus') + msg.setArg(OPENID_NS, 'mode', 'checkid_setup') + msg.setArg(OPENID_NS, 'assoc_handle', 'bogus') + + self.failUnlessRaises(server.ProtocolError, + server.CheckIDRequest.fromMessage, + msg, self.server) + + def test_fromMessageClaimedIDWithoutIdentityOpenID2(self): + name = 'https://example.myopenid.com' + + msg = Message(OPENID2_NS) + msg.setArg(OPENID_NS, 'mode', 'checkid_setup') + msg.setArg(OPENID_NS, 'return_to', 'http://invalid:8000/rt') + msg.setArg(OPENID_NS, 'claimed_id', name) + + self.failUnlessRaises(server.ProtocolError, + server.CheckIDRequest.fromMessage, + msg, self.server) + + def test_fromMessageIdentityWithoutClaimedIDOpenID2(self): + name = 'https://example.myopenid.com' + + msg = Message(OPENID2_NS) + msg.setArg(OPENID_NS, 'mode', 'checkid_setup') + msg.setArg(OPENID_NS, 'return_to', 'http://invalid:8000/rt') + msg.setArg(OPENID_NS, 'identity', name) + + self.failUnlessRaises(server.ProtocolError, + server.CheckIDRequest.fromMessage, + msg, self.server) + + def test_trustRootOpenID1(self): + """Ignore openid.realm in OpenID 1""" + msg = Message(OPENID1_NS) + msg.setArg(OPENID_NS, 'mode', 'checkid_setup') + msg.setArg(OPENID_NS, 'trust_root', 'http://real_trust_root/') + msg.setArg(OPENID_NS, 'realm', 'http://fake_trust_root/') + msg.setArg(OPENID_NS, 'return_to', 'http://real_trust_root/foo') + msg.setArg(OPENID_NS, 'assoc_handle', 'bogus') + msg.setArg(OPENID_NS, 'identity', 'george') + + result = server.CheckIDRequest.fromMessage(msg, self.server.op_endpoint) + + self.failUnless(result.trust_root == 'http://real_trust_root/') + + def test_trustRootOpenID2(self): + """Ignore openid.trust_root in OpenID 2""" + msg = Message(OPENID2_NS) + msg.setArg(OPENID_NS, 'mode', 'checkid_setup') + msg.setArg(OPENID_NS, 'realm', 'http://real_trust_root/') + msg.setArg(OPENID_NS, 'trust_root', 'http://fake_trust_root/') + msg.setArg(OPENID_NS, 'return_to', 'http://real_trust_root/foo') + msg.setArg(OPENID_NS, 'assoc_handle', 'bogus') + msg.setArg(OPENID_NS, 'identity', 'george') + msg.setArg(OPENID_NS, 'claimed_id', 'george') + + result = server.CheckIDRequest.fromMessage(msg, self.server.op_endpoint) + + self.failUnless(result.trust_root == 'http://real_trust_root/') + + def test_answerAllowNoTrustRoot(self): + self.request.trust_root = None + answer = self.request.answer(True) + self.failUnlessEqual(answer.request, self.request) + self._expectAnswer(answer, self.request.identity) + + def test_fromMessageWithoutTrustRoot(self): + msg = Message(OPENID2_NS) + msg.setArg(OPENID_NS, 'mode', 'checkid_setup') + msg.setArg(OPENID_NS, 'return_to', 'http://real_trust_root/foo') + msg.setArg(OPENID_NS, 'assoc_handle', 'bogus') + msg.setArg(OPENID_NS, 'identity', 'george') + msg.setArg(OPENID_NS, 'claimed_id', 'george') + + result = server.CheckIDRequest.fromMessage(msg, self.server.op_endpoint) + + self.failUnlessEqual(result.trust_root, 'http://real_trust_root/foo') + + def test_fromMessageWithEmptyTrustRoot(self): + return_to = u'http://someplace.invalid/?go=thing' + msg = Message.fromPostArgs({ + u'openid.assoc_handle': u'{blah}{blah}{OZivdQ==}', + u'openid.claimed_id': u'http://delegated.invalid/', + u'openid.identity': u'http://op-local.example.com/', + u'openid.mode': u'checkid_setup', + u'openid.ns': u'http://openid.net/signon/1.0', + u'openid.return_to': return_to, + u'openid.trust_root': u''}) + + result = server.CheckIDRequest.fromMessage(msg, self.server.op_endpoint) + + self.failUnlessEqual(result.trust_root, return_to) + + def test_fromMessageWithoutTrustRootOrReturnTo(self): + msg = Message(OPENID2_NS) + msg.setArg(OPENID_NS, 'mode', 'checkid_setup') + msg.setArg(OPENID_NS, 'assoc_handle', 'bogus') + msg.setArg(OPENID_NS, 'identity', 'george') + msg.setArg(OPENID_NS, 'claimed_id', 'george') + + self.failUnlessRaises(server.ProtocolError, + server.CheckIDRequest.fromMessage, + msg, self.server.op_endpoint) + + def test_answerAllowNoEndpointOpenID1(self): + """Test .allow() with an OpenID 1.x Message on a CheckIDRequest + built without an op_endpoint parameter. + """ + identity = 'http://bambam.unittest/' + reqmessage = Message.fromOpenIDArgs({ + 'identity': identity, + 'trust_root': 'http://bar.unittest/', + 'return_to': 'http://bar.unittest/999', + }) + self.request = server.CheckIDRequest.fromMessage(reqmessage, None) + answer = self.request.answer(True) + + expected_list = [ + ('mode', 'id_res'), + ('return_to', self.request.return_to), + ('identity', identity), + ] + + for k, expected in expected_list: + actual = answer.fields.getArg(OPENID_NS, k) + self.failUnlessEqual( + expected, actual, + "%s: expected %s, got %s" % (k, expected, actual)) + + self.failUnless(answer.fields.hasKey(OPENID_NS, 'response_nonce')) + self.failUnlessEqual(answer.fields.getOpenIDNamespace(), OPENID1_NS) + self.failUnless(answer.fields.namespaces.isImplicit(OPENID1_NS)) + + # One for nonce (OpenID v1 namespace is implicit) + self.failUnlessEqual(len(answer.fields.toPostArgs()), + len(expected_list) + 1, + answer.fields.toPostArgs()) + + def test_answerImmediateDenyOpenID2(self): + """Look for mode=setup_needed in checkid_immediate negative + response in OpenID 2 case. + + See specification Responding to Authentication Requests / + Negative Assertions / In Response to Immediate Requests. + """ + self.request.mode = 'checkid_immediate' + self.request.immediate = True + self.request.claimed_id = 'http://claimed-id.test/' + server_url = "http://setup-url.unittest/" + # crappiting setup_url, you dirty my interface with your presence! + answer = self.request.answer(False, server_url=server_url) + self.failUnlessEqual(answer.request, self.request) + self.failUnlessEqual(len(answer.fields.toPostArgs()), 3, answer.fields) + self.failUnlessEqual(answer.fields.getOpenIDNamespace(), OPENID2_NS) + self.failUnlessEqual(answer.fields.getArg(OPENID_NS, 'mode'), + 'setup_needed') + + usu = answer.fields.getArg(OPENID_NS, 'user_setup_url') + expected_substr = 'openid.claimed_id=http%3A%2F%2Fclaimed-id.test%2F' + self.failUnless(expected_substr in usu, usu) + + def test_answerImmediateDenyOpenID1(self): + """Look for user_setup_url in checkid_immediate negative + response in OpenID 1 case.""" + self.request.message = Message(OPENID1_NS) + self.request.mode = 'checkid_immediate' + self.request.immediate = True + server_url = "http://setup-url.unittest/" + # crappiting setup_url, you dirty my interface with your presence! + answer = self.request.answer(False, server_url=server_url) + self.failUnlessEqual(answer.request, self.request) + self.failUnlessEqual(len(answer.fields.toPostArgs()), 2, answer.fields) + self.failUnlessEqual(answer.fields.getOpenIDNamespace(), OPENID1_NS) + self.failUnless(answer.fields.namespaces.isImplicit(OPENID1_NS)) + self.failUnlessEqual(answer.fields.getArg(OPENID_NS, 'mode'), 'id_res') + self.failUnless(answer.fields.getArg( + OPENID_NS, 'user_setup_url', '').startswith(server_url)) + + def test_answerSetupDeny(self): + answer = self.request.answer(False) + self.failUnlessEqual(answer.fields.getArgs(OPENID_NS), { + 'mode': 'cancel', + }) + + def test_encodeToURL(self): + server_url = 'http://openid-server.unittest/' + result = self.request.encodeToURL(server_url) + + # How to check? How about a round-trip test. + base, result_args = result.split('?', 1) + result_args = dict(cgi.parse_qsl(result_args)) + message = Message.fromPostArgs(result_args) + rebuilt_request = server.CheckIDRequest.fromMessage(message, + self.server.op_endpoint) + # argh, lousy hack + self.request.message = message + self.failUnlessEqual(rebuilt_request.__dict__, self.request.__dict__) + + def test_getCancelURL(self): + url = self.request.getCancelURL() + rt, query_string = url.split('?') + self.failUnlessEqual(self.request.return_to, rt) + query = dict(cgi.parse_qsl(query_string)) + self.failUnlessEqual(query, {'openid.mode':'cancel', + 'openid.ns':OPENID2_NS}) + + def test_getCancelURLimmed(self): + self.request.mode = 'checkid_immediate' + self.request.immediate = True + self.failUnlessRaises(ValueError, self.request.getCancelURL) + + + +class TestCheckIDExtension(unittest.TestCase): + + def setUp(self): + self.op_endpoint = 'http://endpoint.unittest/ext' + self.store = memstore.MemoryStore() + self.server = server.Server(self.store, self.op_endpoint) + self.request = server.CheckIDRequest( + identity = 'http://bambam.unittest/', + trust_root = 'http://bar.unittest/', + return_to = 'http://bar.unittest/999', + immediate = False, + op_endpoint = self.server.op_endpoint, + ) + self.request.message = Message(OPENID2_NS) + self.response = server.OpenIDResponse(self.request) + self.response.fields.setArg(OPENID_NS, 'mode', 'id_res') + self.response.fields.setArg(OPENID_NS, 'blue', 'star') + + + def test_addField(self): + namespace = 'something:' + self.response.fields.setArg(namespace, 'bright', 'potato') + self.failUnlessEqual(self.response.fields.getArgs(OPENID_NS), + {'blue': 'star', + 'mode': 'id_res', + }) + + self.failUnlessEqual(self.response.fields.getArgs(namespace), + {'bright':'potato'}) + + + def test_addFields(self): + namespace = 'mi5:' + args = {'tangy': 'suspenders', + 'bravo': 'inclusion'} + self.response.fields.updateArgs(namespace, args) + self.failUnlessEqual(self.response.fields.getArgs(OPENID_NS), + {'blue': 'star', + 'mode': 'id_res', + }) + self.failUnlessEqual(self.response.fields.getArgs(namespace), args) + + + +class MockSignatory(object): + isValid = True + + def __init__(self, assoc): + self.assocs = [assoc] + + def verify(self, assoc_handle, message): + assert message.hasKey(OPENID_NS, "sig") + if (True, assoc_handle) in self.assocs: + return self.isValid + else: + return False + + def getAssociation(self, assoc_handle, dumb): + if (dumb, assoc_handle) in self.assocs: + # This isn't a valid implementation for many uses of this + # function, mind you. + return True + else: + return None + + def invalidate(self, assoc_handle, dumb): + if (dumb, assoc_handle) in self.assocs: + self.assocs.remove((dumb, assoc_handle)) + + +class TestCheckAuth(unittest.TestCase): + def setUp(self): + self.assoc_handle = 'mooooooooo' + self.message = Message.fromPostArgs({ + 'openid.sig': 'signarture', + 'one': 'alpha', + 'two': 'beta', + }) + self.request = server.CheckAuthRequest( + self.assoc_handle, self.message) + + self.signatory = MockSignatory((True, self.assoc_handle)) + + def test_valid(self): + r = self.request.answer(self.signatory) + self.failUnlessEqual(r.fields.getArgs(OPENID_NS), {'is_valid': 'true'}) + self.failUnlessEqual(r.request, self.request) + + def test_invalid(self): + self.signatory.isValid = False + r = self.request.answer(self.signatory) + self.failUnlessEqual(r.fields.getArgs(OPENID_NS), + {'is_valid': 'false'}) + + def test_replay(self): + """Don't validate the same response twice. + + From "Checking the Nonce":: + + When using "check_authentication", the OP MUST ensure that an + assertion has not yet been accepted with the same value for + "openid.response_nonce". + + In this implementation, the assoc_handle is only valid once. And + nonces are a signed component of the message, so they can't be used + with another handle without breaking the sig. + """ + r = self.request.answer(self.signatory) + r = self.request.answer(self.signatory) + self.failUnlessEqual(r.fields.getArgs(OPENID_NS), + {'is_valid': 'false'}) + + def test_invalidatehandle(self): + self.request.invalidate_handle = "bogusHandle" + r = self.request.answer(self.signatory) + self.failUnlessEqual(r.fields.getArgs(OPENID_NS), + {'is_valid': 'true', + 'invalidate_handle': "bogusHandle"}) + self.failUnlessEqual(r.request, self.request) + + def test_invalidatehandleNo(self): + assoc_handle = 'goodhandle' + self.signatory.assocs.append((False, 'goodhandle')) + self.request.invalidate_handle = assoc_handle + r = self.request.answer(self.signatory) + self.failUnlessEqual(r.fields.getArgs(OPENID_NS), {'is_valid': 'true'}) + + +class TestAssociate(unittest.TestCase): + # TODO: test DH with non-default values for modulus and gen. + # (important to do because we actually had it broken for a while.) + + def setUp(self): + self.request = server.AssociateRequest.fromMessage( + Message.fromPostArgs({})) + self.store = memstore.MemoryStore() + self.signatory = server.Signatory(self.store) + + def test_dhSHA1(self): + self.assoc = self.signatory.createAssociation(dumb=False, assoc_type='HMAC-SHA1') + from openid.dh import DiffieHellman + from openid.server.server import DiffieHellmanSHA1ServerSession + consumer_dh = DiffieHellman.fromDefaults() + cpub = consumer_dh.public + server_dh = DiffieHellman.fromDefaults() + session = DiffieHellmanSHA1ServerSession(server_dh, cpub) + self.request = server.AssociateRequest(session, 'HMAC-SHA1') + response = self.request.answer(self.assoc) + rfg = lambda f: response.fields.getArg(OPENID_NS, f) + self.failUnlessEqual(rfg("assoc_type"), "HMAC-SHA1") + self.failUnlessEqual(rfg("assoc_handle"), self.assoc.handle) + self.failIf(rfg("mac_key")) + self.failUnlessEqual(rfg("session_type"), "DH-SHA1") + self.failUnless(rfg("enc_mac_key")) + self.failUnless(rfg("dh_server_public")) + + enc_key = rfg("enc_mac_key").decode('base64') + spub = cryptutil.base64ToLong(rfg("dh_server_public")) + secret = consumer_dh.xorSecret(spub, enc_key, cryptutil.sha1) + self.failUnlessEqual(secret, self.assoc.secret) + + + if not cryptutil.SHA256_AVAILABLE: + warnings.warn("Not running SHA256 tests.") + else: + def test_dhSHA256(self): + self.assoc = self.signatory.createAssociation( + dumb=False, assoc_type='HMAC-SHA256') + from openid.dh import DiffieHellman + from openid.server.server import DiffieHellmanSHA256ServerSession + consumer_dh = DiffieHellman.fromDefaults() + cpub = consumer_dh.public + server_dh = DiffieHellman.fromDefaults() + session = DiffieHellmanSHA256ServerSession(server_dh, cpub) + self.request = server.AssociateRequest(session, 'HMAC-SHA256') + response = self.request.answer(self.assoc) + rfg = lambda f: response.fields.getArg(OPENID_NS, f) + self.failUnlessEqual(rfg("assoc_type"), "HMAC-SHA256") + self.failUnlessEqual(rfg("assoc_handle"), self.assoc.handle) + self.failIf(rfg("mac_key")) + self.failUnlessEqual(rfg("session_type"), "DH-SHA256") + self.failUnless(rfg("enc_mac_key")) + self.failUnless(rfg("dh_server_public")) + + enc_key = rfg("enc_mac_key").decode('base64') + spub = cryptutil.base64ToLong(rfg("dh_server_public")) + secret = consumer_dh.xorSecret(spub, enc_key, cryptutil.sha256) + self.failUnlessEqual(secret, self.assoc.secret) + + def test_protoError256(self): + from openid.consumer.consumer import \ + DiffieHellmanSHA256ConsumerSession + + s256_session = DiffieHellmanSHA256ConsumerSession() + + invalid_s256 = {'openid.assoc_type':'HMAC-SHA1', + 'openid.session_type':'DH-SHA256',} + invalid_s256.update(s256_session.getRequest()) + + invalid_s256_2 = {'openid.assoc_type':'MONKEY-PIRATE', + 'openid.session_type':'DH-SHA256',} + invalid_s256_2.update(s256_session.getRequest()) + + bad_request_argss = [ + invalid_s256, + invalid_s256_2, + ] + + for request_args in bad_request_argss: + message = Message.fromPostArgs(request_args) + self.failUnlessRaises(server.ProtocolError, + server.AssociateRequest.fromMessage, + message) + + def test_protoError(self): + from openid.consumer.consumer import DiffieHellmanSHA1ConsumerSession + + s1_session = DiffieHellmanSHA1ConsumerSession() + + invalid_s1 = {'openid.assoc_type':'HMAC-SHA256', + 'openid.session_type':'DH-SHA1',} + invalid_s1.update(s1_session.getRequest()) + + invalid_s1_2 = {'openid.assoc_type':'ROBOT-NINJA', + 'openid.session_type':'DH-SHA1',} + invalid_s1_2.update(s1_session.getRequest()) + + bad_request_argss = [ + {'openid.assoc_type':'Wha?'}, + invalid_s1, + invalid_s1_2, + ] + + for request_args in bad_request_argss: + message = Message.fromPostArgs(request_args) + self.failUnlessRaises(server.ProtocolError, + server.AssociateRequest.fromMessage, + message) + + def test_protoErrorFields(self): + + contact = 'user@example.invalid' + reference = 'Trac ticket number MAX_INT' + error = 'poltergeist' + + openid1_args = { + 'openid.identitiy': 'invalid', + 'openid.mode': 'checkid_setup', + } + + openid2_args = dict(openid1_args) + openid2_args.update({'openid.ns': OPENID2_NS}) + + # Check presence of optional fields in both protocol versions + + openid1_msg = Message.fromPostArgs(openid1_args) + p = server.ProtocolError(openid1_msg, error, + contact=contact, reference=reference) + reply = p.toMessage() + + self.failUnlessEqual(reply.getArg(OPENID_NS, 'reference'), reference) + self.failUnlessEqual(reply.getArg(OPENID_NS, 'contact'), contact) + + openid2_msg = Message.fromPostArgs(openid2_args) + p = server.ProtocolError(openid2_msg, error, + contact=contact, reference=reference) + reply = p.toMessage() + + self.failUnlessEqual(reply.getArg(OPENID_NS, 'reference'), reference) + self.failUnlessEqual(reply.getArg(OPENID_NS, 'contact'), contact) + + def failUnlessExpiresInMatches(self, msg, expected_expires_in): + expires_in_str = msg.getArg(OPENID_NS, 'expires_in', no_default) + expires_in = int(expires_in_str) + + # Slop is necessary because the tests can sometimes get run + # right on a second boundary + slop = 1 # second + difference = expected_expires_in - expires_in + + error_message = ('"expires_in" value not within %s of expected: ' + 'expected=%s, actual=%s' % + (slop, expected_expires_in, expires_in)) + self.failUnless(0 <= difference <= slop, error_message) + + def test_plaintext(self): + self.assoc = self.signatory.createAssociation(dumb=False, assoc_type='HMAC-SHA1') + response = self.request.answer(self.assoc) + rfg = lambda f: response.fields.getArg(OPENID_NS, f) + + self.failUnlessEqual(rfg("assoc_type"), "HMAC-SHA1") + self.failUnlessEqual(rfg("assoc_handle"), self.assoc.handle) + + self.failUnlessExpiresInMatches( + response.fields, self.signatory.SECRET_LIFETIME) + + self.failUnlessEqual( + rfg("mac_key"), oidutil.toBase64(self.assoc.secret)) + self.failIf(rfg("session_type")) + self.failIf(rfg("enc_mac_key")) + self.failIf(rfg("dh_server_public")) + + def test_plaintext_v2(self): + # The main difference between this and the v1 test is that + # session_type is always returned in v2. + args = { + 'openid.ns': OPENID2_NS, + 'openid.mode': 'associate', + 'openid.assoc_type': 'HMAC-SHA1', + 'openid.session_type': 'no-encryption', + } + self.request = server.AssociateRequest.fromMessage( + Message.fromPostArgs(args)) + + self.failIf(self.request.message.isOpenID1()) + + self.assoc = self.signatory.createAssociation( + dumb=False, assoc_type='HMAC-SHA1') + response = self.request.answer(self.assoc) + rfg = lambda f: response.fields.getArg(OPENID_NS, f) + + self.failUnlessEqual(rfg("assoc_type"), "HMAC-SHA1") + self.failUnlessEqual(rfg("assoc_handle"), self.assoc.handle) + + self.failUnlessExpiresInMatches( + response.fields, self.signatory.SECRET_LIFETIME) + + self.failUnlessEqual( + rfg("mac_key"), oidutil.toBase64(self.assoc.secret)) + + self.failUnlessEqual(rfg("session_type"), "no-encryption") + self.failIf(rfg("enc_mac_key")) + self.failIf(rfg("dh_server_public")) + + def test_plaintext256(self): + self.assoc = self.signatory.createAssociation(dumb=False, assoc_type='HMAC-SHA256') + response = self.request.answer(self.assoc) + rfg = lambda f: response.fields.getArg(OPENID_NS, f) + + self.failUnlessEqual(rfg("assoc_type"), "HMAC-SHA1") + self.failUnlessEqual(rfg("assoc_handle"), self.assoc.handle) + + self.failUnlessExpiresInMatches( + response.fields, self.signatory.SECRET_LIFETIME) + + self.failUnlessEqual( + rfg("mac_key"), oidutil.toBase64(self.assoc.secret)) + self.failIf(rfg("session_type")) + self.failIf(rfg("enc_mac_key")) + self.failIf(rfg("dh_server_public")) + + def test_unsupportedPrefer(self): + allowed_assoc = 'COLD-PET-RAT' + allowed_sess = 'FROG-BONES' + message = 'This is a unit test' + + # Set an OpenID 2 message so answerUnsupported doesn't raise + # ProtocolError. + self.request.message = Message(OPENID2_NS) + + response = self.request.answerUnsupported( + message=message, + preferred_session_type=allowed_sess, + preferred_association_type=allowed_assoc, + ) + rfg = lambda f: response.fields.getArg(OPENID_NS, f) + self.failUnlessEqual(rfg('error_code'), 'unsupported-type') + self.failUnlessEqual(rfg('assoc_type'), allowed_assoc) + self.failUnlessEqual(rfg('error'), message) + self.failUnlessEqual(rfg('session_type'), allowed_sess) + + def test_unsupported(self): + message = 'This is a unit test' + + # Set an OpenID 2 message so answerUnsupported doesn't raise + # ProtocolError. + self.request.message = Message(OPENID2_NS) + + response = self.request.answerUnsupported(message) + rfg = lambda f: response.fields.getArg(OPENID_NS, f) + self.failUnlessEqual(rfg('error_code'), 'unsupported-type') + self.failUnlessEqual(rfg('assoc_type'), None) + self.failUnlessEqual(rfg('error'), message) + self.failUnlessEqual(rfg('session_type'), None) + +class Counter(object): + def __init__(self): + self.count = 0 + + def inc(self): + self.count += 1 + +class TestServer(unittest.TestCase, CatchLogs): + def setUp(self): + self.store = memstore.MemoryStore() + self.server = server.Server(self.store, "http://server.unittest/endpt") + CatchLogs.setUp(self) + + def test_dispatch(self): + monkeycalled = Counter() + def monkeyDo(request): + monkeycalled.inc() + r = server.OpenIDResponse(request) + return r + self.server.openid_monkeymode = monkeyDo + request = server.OpenIDRequest() + request.mode = "monkeymode" + request.namespace = OPENID1_NS + webresult = self.server.handleRequest(request) + self.failUnlessEqual(monkeycalled.count, 1) + + def test_associate(self): + request = server.AssociateRequest.fromMessage(Message.fromPostArgs({})) + response = self.server.openid_associate(request) + self.failUnless(response.fields.hasKey(OPENID_NS, "assoc_handle"), + "No assoc_handle here: %s" % (response.fields,)) + + def test_associate2(self): + """Associate when the server has no allowed association types + + Gives back an error with error_code and no fallback session or + assoc types.""" + self.server.negotiator.setAllowedTypes([]) + + # Set an OpenID 2 message so answerUnsupported doesn't raise + # ProtocolError. + msg = Message.fromPostArgs({ + 'openid.ns': OPENID2_NS, + 'openid.session_type': 'no-encryption', + }) + + request = server.AssociateRequest.fromMessage(msg) + + response = self.server.openid_associate(request) + self.failUnless(response.fields.hasKey(OPENID_NS, "error")) + self.failUnless(response.fields.hasKey(OPENID_NS, "error_code")) + self.failIf(response.fields.hasKey(OPENID_NS, "assoc_handle")) + self.failIf(response.fields.hasKey(OPENID_NS, "assoc_type")) + self.failIf(response.fields.hasKey(OPENID_NS, "session_type")) + + def test_associate3(self): + """Request an assoc type that is not supported when there are + supported types. + + Should give back an error message with a fallback type. + """ + self.server.negotiator.setAllowedTypes([('HMAC-SHA256', 'DH-SHA256')]) + + msg = Message.fromPostArgs({ + 'openid.ns': OPENID2_NS, + 'openid.session_type': 'no-encryption', + }) + + request = server.AssociateRequest.fromMessage(msg) + response = self.server.openid_associate(request) + + self.failUnless(response.fields.hasKey(OPENID_NS, "error")) + self.failUnless(response.fields.hasKey(OPENID_NS, "error_code")) + self.failIf(response.fields.hasKey(OPENID_NS, "assoc_handle")) + self.failUnlessEqual(response.fields.getArg(OPENID_NS, "assoc_type"), + 'HMAC-SHA256') + self.failUnlessEqual(response.fields.getArg(OPENID_NS, "session_type"), + 'DH-SHA256') + + if not cryptutil.SHA256_AVAILABLE: + warnings.warn("Not running SHA256 tests.") + else: + def test_associate4(self): + """DH-SHA256 association session""" + self.server.negotiator.setAllowedTypes( + [('HMAC-SHA256', 'DH-SHA256')]) + query = { + 'openid.dh_consumer_public': + 'ALZgnx8N5Lgd7pCj8K86T/DDMFjJXSss1SKoLmxE72kJTzOtG6I2PaYrHX' + 'xku4jMQWSsGfLJxwCZ6280uYjUST/9NWmuAfcrBfmDHIBc3H8xh6RBnlXJ' + '1WxJY3jHd5k1/ZReyRZOxZTKdF/dnIqwF8ZXUwI6peV0TyS/K1fOfF/s', + 'openid.assoc_type': 'HMAC-SHA256', + 'openid.session_type': 'DH-SHA256', + } + message = Message.fromPostArgs(query) + request = server.AssociateRequest.fromMessage(message) + response = self.server.openid_associate(request) + self.failUnless(response.fields.hasKey(OPENID_NS, "assoc_handle")) + + def test_missingSessionTypeOpenID2(self): + """Make sure session_type is required in OpenID 2""" + msg = Message.fromPostArgs({ + 'openid.ns': OPENID2_NS, + }) + + self.assertRaises(server.ProtocolError, + server.AssociateRequest.fromMessage, msg) + + def test_checkAuth(self): + request = server.CheckAuthRequest('arrrrrf', '0x3999', []) + response = self.server.openid_check_authentication(request) + self.failUnless(response.fields.hasKey(OPENID_NS, "is_valid")) + +class TestSignatory(unittest.TestCase, CatchLogs): + def setUp(self): + self.store = memstore.MemoryStore() + self.signatory = server.Signatory(self.store) + self._dumb_key = self.signatory._dumb_key + self._normal_key = self.signatory._normal_key + CatchLogs.setUp(self) + + def test_sign(self): + request = server.OpenIDRequest() + assoc_handle = '{assoc}{lookatme}' + self.store.storeAssociation( + self._normal_key, + association.Association.fromExpiresIn(60, assoc_handle, + 'sekrit', 'HMAC-SHA1')) + request.assoc_handle = assoc_handle + request.namespace = OPENID1_NS + response = server.OpenIDResponse(request) + response.fields = Message.fromOpenIDArgs({ + 'foo': 'amsigned', + 'bar': 'notsigned', + 'azu': 'alsosigned', + }) + sresponse = self.signatory.sign(response) + self.failUnlessEqual( + sresponse.fields.getArg(OPENID_NS, 'assoc_handle'), + assoc_handle) + self.failUnlessEqual(sresponse.fields.getArg(OPENID_NS, 'signed'), + 'assoc_handle,azu,bar,foo,signed') + self.failUnless(sresponse.fields.getArg(OPENID_NS, 'sig')) + self.failIf(self.messages, self.messages) + + def test_signDumb(self): + request = server.OpenIDRequest() + request.assoc_handle = None + request.namespace = OPENID2_NS + response = server.OpenIDResponse(request) + response.fields = Message.fromOpenIDArgs({ + 'foo': 'amsigned', + 'bar': 'notsigned', + 'azu': 'alsosigned', + 'ns':OPENID2_NS, + }) + sresponse = self.signatory.sign(response) + assoc_handle = sresponse.fields.getArg(OPENID_NS, 'assoc_handle') + self.failUnless(assoc_handle) + assoc = self.signatory.getAssociation(assoc_handle, dumb=True) + self.failUnless(assoc) + self.failUnlessEqual(sresponse.fields.getArg(OPENID_NS, 'signed'), + 'assoc_handle,azu,bar,foo,ns,signed') + self.failUnless(sresponse.fields.getArg(OPENID_NS, 'sig')) + self.failIf(self.messages, self.messages) + + def test_signExpired(self): + """Sign a response to a message with an expired handle (using invalidate_handle). + + From "Verifying with an Association":: + + If an authentication request included an association handle for an + association between the OP and the Relying party, and the OP no + longer wishes to use that handle (because it has expired or the + secret has been compromised, for instance), the OP will send a + response that must be verified directly with the OP, as specified + in Section 11.3.2. In that instance, the OP will include the field + "openid.invalidate_handle" set to the association handle that the + Relying Party included with the original request. + """ + request = server.OpenIDRequest() + request.namespace = OPENID2_NS + assoc_handle = '{assoc}{lookatme}' + self.store.storeAssociation( + self._normal_key, + association.Association.fromExpiresIn(-10, assoc_handle, + 'sekrit', 'HMAC-SHA1')) + self.failUnless(self.store.getAssociation(self._normal_key, assoc_handle)) + + request.assoc_handle = assoc_handle + response = server.OpenIDResponse(request) + response.fields = Message.fromOpenIDArgs({ + 'foo': 'amsigned', + 'bar': 'notsigned', + 'azu': 'alsosigned', + }) + sresponse = self.signatory.sign(response) + + new_assoc_handle = sresponse.fields.getArg(OPENID_NS, 'assoc_handle') + self.failUnless(new_assoc_handle) + self.failIfEqual(new_assoc_handle, assoc_handle) + + self.failUnlessEqual( + sresponse.fields.getArg(OPENID_NS, 'invalidate_handle'), + assoc_handle) + + self.failUnlessEqual(sresponse.fields.getArg(OPENID_NS, 'signed'), + 'assoc_handle,azu,bar,foo,invalidate_handle,signed') + self.failUnless(sresponse.fields.getArg(OPENID_NS, 'sig')) + + # make sure the expired association is gone + self.failIf(self.store.getAssociation(self._normal_key, assoc_handle), + "expired association is still retrievable.") + + # make sure the new key is a dumb mode association + self.failUnless(self.store.getAssociation(self._dumb_key, new_assoc_handle)) + self.failIf(self.store.getAssociation(self._normal_key, new_assoc_handle)) + self.failUnless(self.messages) + + + def test_signInvalidHandle(self): + request = server.OpenIDRequest() + request.namespace = OPENID2_NS + assoc_handle = '{bogus-assoc}{notvalid}' + + request.assoc_handle = assoc_handle + response = server.OpenIDResponse(request) + response.fields = Message.fromOpenIDArgs({ + 'foo': 'amsigned', + 'bar': 'notsigned', + 'azu': 'alsosigned', + }) + sresponse = self.signatory.sign(response) + + new_assoc_handle = sresponse.fields.getArg(OPENID_NS, 'assoc_handle') + self.failUnless(new_assoc_handle) + self.failIfEqual(new_assoc_handle, assoc_handle) + + self.failUnlessEqual( + sresponse.fields.getArg(OPENID_NS, 'invalidate_handle'), + assoc_handle) + + self.failUnlessEqual( + sresponse.fields.getArg(OPENID_NS, 'signed'), 'assoc_handle,azu,bar,foo,invalidate_handle,signed') + self.failUnless(sresponse.fields.getArg(OPENID_NS, 'sig')) + + # make sure the new key is a dumb mode association + self.failUnless(self.store.getAssociation(self._dumb_key, new_assoc_handle)) + self.failIf(self.store.getAssociation(self._normal_key, new_assoc_handle)) + self.failIf(self.messages, self.messages) + + + def test_verify(self): + assoc_handle = '{vroom}{zoom}' + assoc = association.Association.fromExpiresIn( + 60, assoc_handle, 'sekrit', 'HMAC-SHA1') + + self.store.storeAssociation(self._dumb_key, assoc) + + signed = Message.fromPostArgs({ + 'openid.foo': 'bar', + 'openid.apple': 'orange', + 'openid.assoc_handle': assoc_handle, + 'openid.signed': 'apple,assoc_handle,foo,signed', + 'openid.sig': 'uXoT1qm62/BB09Xbj98TQ8mlBco=', + }) + + verified = self.signatory.verify(assoc_handle, signed) + self.failIf(self.messages, self.messages) + self.failUnless(verified) + + + def test_verifyBadSig(self): + assoc_handle = '{vroom}{zoom}' + assoc = association.Association.fromExpiresIn( + 60, assoc_handle, 'sekrit', 'HMAC-SHA1') + + self.store.storeAssociation(self._dumb_key, assoc) + + signed = Message.fromPostArgs({ + 'openid.foo': 'bar', + 'openid.apple': 'orange', + 'openid.assoc_handle': assoc_handle, + 'openid.signed': 'apple,assoc_handle,foo,signed', + 'openid.sig': 'uXoT1qm62/BB09Xbj98TQ8mlBco='.encode('rot13'), + }) + + verified = self.signatory.verify(assoc_handle, signed) + self.failIf(self.messages, self.messages) + self.failIf(verified) + + def test_verifyBadHandle(self): + assoc_handle = '{vroom}{zoom}' + signed = Message.fromPostArgs({ + 'foo': 'bar', + 'apple': 'orange', + 'openid.sig': "Ylu0KcIR7PvNegB/K41KpnRgJl0=", + }) + + verified = self.signatory.verify(assoc_handle, signed) + self.failIf(verified) + self.failUnless(self.messages) + + + def test_verifyAssocMismatch(self): + """Attempt to validate sign-all message with a signed-list assoc.""" + assoc_handle = '{vroom}{zoom}' + assoc = association.Association.fromExpiresIn( + 60, assoc_handle, 'sekrit', 'HMAC-SHA1') + + self.store.storeAssociation(self._dumb_key, assoc) + + signed = Message.fromPostArgs({ + 'foo': 'bar', + 'apple': 'orange', + 'openid.sig': "d71xlHtqnq98DonoSgoK/nD+QRM=", + }) + + verified = self.signatory.verify(assoc_handle, signed) + self.failIf(verified) + self.failUnless(self.messages) + + def test_getAssoc(self): + assoc_handle = self.makeAssoc(dumb=True) + assoc = self.signatory.getAssociation(assoc_handle, True) + self.failUnless(assoc) + self.failUnlessEqual(assoc.handle, assoc_handle) + self.failIf(self.messages, self.messages) + + def test_getAssocExpired(self): + assoc_handle = self.makeAssoc(dumb=True, lifetime=-10) + assoc = self.signatory.getAssociation(assoc_handle, True) + self.failIf(assoc, assoc) + self.failUnless(self.messages) + + def test_getAssocInvalid(self): + ah = 'no-such-handle' + self.failUnlessEqual( + self.signatory.getAssociation(ah, dumb=False), None) + self.failIf(self.messages, self.messages) + + def test_getAssocDumbVsNormal(self): + """getAssociation(dumb=False) cannot get a dumb assoc""" + assoc_handle = self.makeAssoc(dumb=True) + self.failUnlessEqual( + self.signatory.getAssociation(assoc_handle, dumb=False), None) + self.failIf(self.messages, self.messages) + + def test_getAssocNormalVsDumb(self): + """getAssociation(dumb=True) cannot get a shared assoc + + From "Verifying Directly with the OpenID Provider":: + + An OP MUST NOT verify signatures for associations that have shared + MAC keys. + """ + assoc_handle = self.makeAssoc(dumb=False) + self.failUnlessEqual( + self.signatory.getAssociation(assoc_handle, dumb=True), None) + self.failIf(self.messages, self.messages) + + def test_createAssociation(self): + assoc = self.signatory.createAssociation(dumb=False) + self.failUnless(self.signatory.getAssociation(assoc.handle, dumb=False)) + self.failIf(self.messages, self.messages) + + def makeAssoc(self, dumb, lifetime=60): + assoc_handle = '{bling}' + assoc = association.Association.fromExpiresIn(lifetime, assoc_handle, + 'sekrit', 'HMAC-SHA1') + + self.store.storeAssociation((dumb and self._dumb_key) or self._normal_key, assoc) + return assoc_handle + + def test_invalidate(self): + assoc_handle = '-squash-' + assoc = association.Association.fromExpiresIn(60, assoc_handle, + 'sekrit', 'HMAC-SHA1') + + self.store.storeAssociation(self._dumb_key, assoc) + assoc = self.signatory.getAssociation(assoc_handle, dumb=True) + self.failUnless(assoc) + assoc = self.signatory.getAssociation(assoc_handle, dumb=True) + self.failUnless(assoc) + self.signatory.invalidate(assoc_handle, dumb=True) + assoc = self.signatory.getAssociation(assoc_handle, dumb=True) + self.failIf(assoc) + self.failIf(self.messages, self.messages) + + + +if __name__ == '__main__': + unittest.main() diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_services.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_services.py new file mode 100644 index 0000000..8708a3c --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_services.py @@ -0,0 +1,23 @@ +import unittest + +from openid.yadis import services +from openid.yadis.discover import DiscoveryFailure, DiscoveryResult + + +class TestGetServiceEndpoints(unittest.TestCase): + def setUp(self): + self.orig_discover = services.discover + services.discover = self.discover + + def tearDown(self): + services.discover = self.orig_discover + + def discover(self, input_url): + result = DiscoveryResult(input_url) + result.response_text = "This is not XRDS text." + return result + + def test_catchXRDSError(self): + self.failUnlessRaises(DiscoveryFailure, + services.getServiceEndpoints, + "http://example.invalid/sometest") diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_sreg.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_sreg.py new file mode 100644 index 0000000..49f9ea9 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_sreg.py @@ -0,0 +1,484 @@ +from openid.extensions import sreg +from openid.message import NamespaceMap, Message, registerNamespaceAlias +from openid.server.server import OpenIDRequest, OpenIDResponse + +import unittest + +class SRegURITest(unittest.TestCase): + def test_is11(self): + self.failUnlessEqual(sreg.ns_uri_1_1, sreg.ns_uri) + +class CheckFieldNameTest(unittest.TestCase): + def test_goodNamePasses(self): + for field_name in sreg.data_fields: + sreg.checkFieldName(field_name) + + def test_badNameFails(self): + self.failUnlessRaises(ValueError, sreg.checkFieldName, 'INVALID') + + def test_badTypeFails(self): + self.failUnlessRaises(ValueError, sreg.checkFieldName, None) + +# For supportsSReg test +class FakeEndpoint(object): + def __init__(self, supported): + self.supported = supported + self.checked_uris = [] + + def usesExtension(self, namespace_uri): + self.checked_uris.append(namespace_uri) + return namespace_uri in self.supported + +class SupportsSRegTest(unittest.TestCase): + def test_unsupported(self): + endpoint = FakeEndpoint([]) + self.failIf(sreg.supportsSReg(endpoint)) + self.failUnlessEqual([sreg.ns_uri_1_1, sreg.ns_uri_1_0], + endpoint.checked_uris) + + def test_supported_1_1(self): + endpoint = FakeEndpoint([sreg.ns_uri_1_1]) + self.failUnless(sreg.supportsSReg(endpoint)) + self.failUnlessEqual([sreg.ns_uri_1_1], endpoint.checked_uris) + + def test_supported_1_0(self): + endpoint = FakeEndpoint([sreg.ns_uri_1_0]) + self.failUnless(sreg.supportsSReg(endpoint)) + self.failUnlessEqual([sreg.ns_uri_1_1, sreg.ns_uri_1_0], + endpoint.checked_uris) + +class FakeMessage(object): + def __init__(self): + self.openid1 = False + self.namespaces = NamespaceMap() + + def isOpenID1(self): + return self.openid1 + +class GetNSTest(unittest.TestCase): + def setUp(self): + self.msg = FakeMessage() + + def test_openID2Empty(self): + ns_uri = sreg.getSRegNS(self.msg) + self.failUnlessEqual(self.msg.namespaces.getAlias(ns_uri), 'sreg') + self.failUnlessEqual(sreg.ns_uri, ns_uri) + + def test_openID1Empty(self): + self.msg.openid1 = True + ns_uri = sreg.getSRegNS(self.msg) + self.failUnlessEqual(self.msg.namespaces.getAlias(ns_uri), 'sreg') + self.failUnlessEqual(sreg.ns_uri, ns_uri) + + def test_openID1Defined_1_0(self): + self.msg.openid1 = True + self.msg.namespaces.add(sreg.ns_uri_1_0) + ns_uri = sreg.getSRegNS(self.msg) + self.failUnlessEqual(sreg.ns_uri_1_0, ns_uri) + + def test_openID1Defined_1_0_overrideAlias(self): + for openid_version in [True, False]: + for sreg_version in [sreg.ns_uri_1_0, sreg.ns_uri_1_1]: + for alias in ['sreg', 'bogus']: + self.setUp() + + self.msg.openid1 = openid_version + self.msg.namespaces.addAlias(sreg_version, alias) + ns_uri = sreg.getSRegNS(self.msg) + self.failUnlessEqual(self.msg.namespaces.getAlias(ns_uri), alias) + self.failUnlessEqual(sreg_version, ns_uri) + + def test_openID1DefinedBadly(self): + self.msg.openid1 = True + self.msg.namespaces.addAlias('http://invalid/', 'sreg') + self.failUnlessRaises(sreg.SRegNamespaceError, + sreg.getSRegNS, self.msg) + + def test_openID2DefinedBadly(self): + self.msg.openid1 = False + self.msg.namespaces.addAlias('http://invalid/', 'sreg') + self.failUnlessRaises(sreg.SRegNamespaceError, + sreg.getSRegNS, self.msg) + + def test_openID2Defined_1_0(self): + self.msg.namespaces.add(sreg.ns_uri_1_0) + ns_uri = sreg.getSRegNS(self.msg) + self.failUnlessEqual(sreg.ns_uri_1_0, ns_uri) + + def test_openID1_sregNSfromArgs(self): + args = { + 'sreg.optional': 'nickname', + 'sreg.required': 'dob', + } + + m = Message.fromOpenIDArgs(args) + + self.failUnless(m.getArg(sreg.ns_uri_1_1, 'optional') == 'nickname') + self.failUnless(m.getArg(sreg.ns_uri_1_1, 'required') == 'dob') + +class SRegRequestTest(unittest.TestCase): + def test_constructEmpty(self): + req = sreg.SRegRequest() + self.failUnlessEqual([], req.optional) + self.failUnlessEqual([], req.required) + self.failUnlessEqual(None, req.policy_url) + self.failUnlessEqual(sreg.ns_uri, req.ns_uri) + + def test_constructFields(self): + req = sreg.SRegRequest( + ['nickname'], + ['gender'], + 'http://policy', + 'http://sreg.ns_uri') + self.failUnlessEqual(['gender'], req.optional) + self.failUnlessEqual(['nickname'], req.required) + self.failUnlessEqual('http://policy', req.policy_url) + self.failUnlessEqual('http://sreg.ns_uri', req.ns_uri) + + def test_constructBadFields(self): + self.failUnlessRaises( + ValueError, + sreg.SRegRequest, ['elvis']) + + def test_fromOpenIDRequest(self): + args = {} + ns_sentinel = object() + args_sentinel = object() + + class FakeMessage(object): + copied = False + + def __init__(self): + self.message = Message() + + def getArgs(msg_self, ns_uri): + self.failUnlessEqual(ns_sentinel, ns_uri) + return args_sentinel + + def copy(msg_self): + msg_self.copied = True + return msg_self + + class TestingReq(sreg.SRegRequest): + def _getSRegNS(req_self, unused): + return ns_sentinel + + def parseExtensionArgs(req_self, args): + self.failUnlessEqual(args_sentinel, args) + + openid_req = OpenIDRequest() + + msg = FakeMessage() + openid_req.message = msg + + req = TestingReq.fromOpenIDRequest(openid_req) + self.failUnless(type(req) is TestingReq) + self.failUnless(msg.copied) + + def test_parseExtensionArgs_empty(self): + req = sreg.SRegRequest() + results = req.parseExtensionArgs({}) + self.failUnlessEqual(None, results) + + def test_parseExtensionArgs_extraIgnored(self): + req = sreg.SRegRequest() + req.parseExtensionArgs({'janrain':'inc'}) + + def test_parseExtensionArgs_nonStrict(self): + req = sreg.SRegRequest() + req.parseExtensionArgs({'required':'beans'}) + self.failUnlessEqual([], req.required) + + def test_parseExtensionArgs_strict(self): + req = sreg.SRegRequest() + self.failUnlessRaises( + ValueError, + req.parseExtensionArgs, {'required':'beans'}, strict=True) + + def test_parseExtensionArgs_policy(self): + req = sreg.SRegRequest() + req.parseExtensionArgs({'policy_url':'http://policy'}, strict=True) + self.failUnlessEqual('http://policy', req.policy_url) + + def test_parseExtensionArgs_requiredEmpty(self): + req = sreg.SRegRequest() + req.parseExtensionArgs({'required':''}, strict=True) + self.failUnlessEqual([], req.required) + + def test_parseExtensionArgs_optionalEmpty(self): + req = sreg.SRegRequest() + req.parseExtensionArgs({'optional':''}, strict=True) + self.failUnlessEqual([], req.optional) + + def test_parseExtensionArgs_optionalSingle(self): + req = sreg.SRegRequest() + req.parseExtensionArgs({'optional':'nickname'}, strict=True) + self.failUnlessEqual(['nickname'], req.optional) + + def test_parseExtensionArgs_optionalList(self): + req = sreg.SRegRequest() + req.parseExtensionArgs({'optional':'nickname,email'}, strict=True) + self.failUnlessEqual(['nickname','email'], req.optional) + + def test_parseExtensionArgs_optionalListBadNonStrict(self): + req = sreg.SRegRequest() + req.parseExtensionArgs({'optional':'nickname,email,beer'}) + self.failUnlessEqual(['nickname','email'], req.optional) + + def test_parseExtensionArgs_optionalListBadStrict(self): + req = sreg.SRegRequest() + self.failUnlessRaises( + ValueError, + req.parseExtensionArgs, {'optional':'nickname,email,beer'}, + strict=True) + + def test_parseExtensionArgs_bothNonStrict(self): + req = sreg.SRegRequest() + req.parseExtensionArgs({'optional':'nickname', + 'required':'nickname'}) + self.failUnlessEqual([], req.optional) + self.failUnlessEqual(['nickname'], req.required) + + def test_parseExtensionArgs_bothStrict(self): + req = sreg.SRegRequest() + self.failUnlessRaises( + ValueError, + req.parseExtensionArgs, + {'optional':'nickname', + 'required':'nickname'}, + strict=True) + + def test_parseExtensionArgs_bothList(self): + req = sreg.SRegRequest() + req.parseExtensionArgs({'optional':'nickname,email', + 'required':'country,postcode'}, strict=True) + self.failUnlessEqual(['nickname','email'], req.optional) + self.failUnlessEqual(['country','postcode'], req.required) + + def test_allRequestedFields(self): + req = sreg.SRegRequest() + self.failUnlessEqual([], req.allRequestedFields()) + req.requestField('nickname') + self.failUnlessEqual(['nickname'], req.allRequestedFields()) + req.requestField('gender', required=True) + requested = req.allRequestedFields() + requested.sort() + self.failUnlessEqual(['gender', 'nickname'], requested) + + def test_wereFieldsRequested(self): + req = sreg.SRegRequest() + self.failIf(req.wereFieldsRequested()) + req.requestField('gender') + self.failUnless(req.wereFieldsRequested()) + + def test_contains(self): + req = sreg.SRegRequest() + for field_name in sreg.data_fields: + self.failIf(field_name in req) + + self.failIf('something else' in req) + + req.requestField('nickname') + for field_name in sreg.data_fields: + if field_name == 'nickname': + self.failUnless(field_name in req) + else: + self.failIf(field_name in req) + + def test_requestField_bogus(self): + req = sreg.SRegRequest() + self.failUnlessRaises( + ValueError, + req.requestField, 'something else') + + self.failUnlessRaises( + ValueError, + req.requestField, 'something else', strict=True) + + def test_requestField(self): + # Add all of the fields, one at a time + req = sreg.SRegRequest() + fields = list(sreg.data_fields) + for field_name in fields: + req.requestField(field_name) + + self.failUnlessEqual(fields, req.optional) + self.failUnlessEqual([], req.required) + + # By default, adding the same fields over again has no effect + for field_name in fields: + req.requestField(field_name) + + self.failUnlessEqual(fields, req.optional) + self.failUnlessEqual([], req.required) + + # Requesting a field as required overrides requesting it as optional + expected = list(fields) + overridden = expected.pop(0) + req.requestField(overridden, required=True) + self.failUnlessEqual(expected, req.optional) + self.failUnlessEqual([overridden], req.required) + + # Requesting a field as required overrides requesting it as optional + for field_name in fields: + req.requestField(field_name, required=True) + + self.failUnlessEqual([], req.optional) + self.failUnlessEqual(fields, req.required) + + # Requesting it as optional does not downgrade it to optional + for field_name in fields: + req.requestField(field_name) + + self.failUnlessEqual([], req.optional) + self.failUnlessEqual(fields, req.required) + + def test_requestFields_type(self): + req = sreg.SRegRequest() + self.failUnlessRaises(TypeError, req.requestFields, 'nickname') + + def test_requestFields(self): + # Add all of the fields + req = sreg.SRegRequest() + + fields = list(sreg.data_fields) + req.requestFields(fields) + + self.failUnlessEqual(fields, req.optional) + self.failUnlessEqual([], req.required) + + # By default, adding the same fields over again has no effect + req.requestFields(fields) + + self.failUnlessEqual(fields, req.optional) + self.failUnlessEqual([], req.required) + + # Requesting a field as required overrides requesting it as optional + expected = list(fields) + overridden = expected.pop(0) + req.requestFields([overridden], required=True) + self.failUnlessEqual(expected, req.optional) + self.failUnlessEqual([overridden], req.required) + + # Requesting a field as required overrides requesting it as optional + req.requestFields(fields, required=True) + + self.failUnlessEqual([], req.optional) + self.failUnlessEqual(fields, req.required) + + # Requesting it as optional does not downgrade it to optional + req.requestFields(fields) + + self.failUnlessEqual([], req.optional) + self.failUnlessEqual(fields, req.required) + + def test_getExtensionArgs(self): + req = sreg.SRegRequest() + self.failUnlessEqual({}, req.getExtensionArgs()) + + req.requestField('nickname') + self.failUnlessEqual({'optional':'nickname'}, req.getExtensionArgs()) + + req.requestField('email') + self.failUnlessEqual({'optional':'nickname,email'}, + req.getExtensionArgs()) + + req.requestField('gender', required=True) + self.failUnlessEqual({'optional':'nickname,email', + 'required':'gender'}, + req.getExtensionArgs()) + + req.requestField('postcode', required=True) + self.failUnlessEqual({'optional':'nickname,email', + 'required':'gender,postcode'}, + req.getExtensionArgs()) + + req.policy_url = 'http://policy.invalid/' + self.failUnlessEqual({'optional':'nickname,email', + 'required':'gender,postcode', + 'policy_url':'http://policy.invalid/'}, + req.getExtensionArgs()) + +data = { + 'nickname':'linusaur', + 'postcode':'12345', + 'country':'US', + 'gender':'M', + 'fullname':'Leonhard Euler', + 'email':'president@whitehouse.gov', + 'dob':'0000-00-00', + 'language':'en-us', + } + +class DummySuccessResponse(object): + def __init__(self, message, signed_stuff): + self.message = message + self.signed_stuff = signed_stuff + + def getSignedNS(self, ns_uri): + return self.signed_stuff + +class SRegResponseTest(unittest.TestCase): + def test_construct(self): + resp = sreg.SRegResponse(data) + + self.failUnless(resp) + + empty_resp = sreg.SRegResponse({}) + self.failIf(empty_resp) + + # XXX: finish this test + + def test_fromSuccessResponse_signed(self): + message = Message.fromOpenIDArgs({ + 'sreg.nickname':'The Mad Stork', + }) + success_resp = DummySuccessResponse(message, {}) + sreg_resp = sreg.SRegResponse.fromSuccessResponse(success_resp) + self.failIf(sreg_resp) + + def test_fromSuccessResponse_unsigned(self): + message = Message.fromOpenIDArgs({ + 'sreg.nickname':'The Mad Stork', + }) + success_resp = DummySuccessResponse(message, {}) + sreg_resp = sreg.SRegResponse.fromSuccessResponse(success_resp, + signed_only=False) + self.failUnlessEqual([('nickname', 'The Mad Stork')], + sreg_resp.items()) + +class SendFieldsTest(unittest.TestCase): + def test(self): + # Create a request message with simple registration fields + sreg_req = sreg.SRegRequest(required=['nickname', 'email'], + optional=['fullname']) + req_msg = Message() + req_msg.updateArgs(sreg.ns_uri, sreg_req.getExtensionArgs()) + + req = OpenIDRequest() + req.message = req_msg + req.namespace = req_msg.getOpenIDNamespace() + + # -> send checkid_* request + + # Create an empty response message + resp_msg = Message() + resp = OpenIDResponse(req) + resp.fields = resp_msg + + # Put the requested data fields in the response message + sreg_resp = sreg.SRegResponse.extractResponse(sreg_req, data) + resp.addExtension(sreg_resp) + + # <- send id_res response + + # Extract the fields that were sent + sreg_data_resp = resp_msg.getArgs(sreg.ns_uri) + self.failUnlessEqual( + {'nickname':'linusaur', + 'email':'president@whitehouse.gov', + 'fullname':'Leonhard Euler', + }, sreg_data_resp) + +if __name__ == '__main__': + unittest.main() diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_symbol.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_symbol.py new file mode 100644 index 0000000..42ca212 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_symbol.py @@ -0,0 +1,35 @@ +import unittest + +from openid import oidutil + +class SymbolTest(unittest.TestCase): + def test_selfEquality(self): + s = oidutil.Symbol('xxx') + self.failUnlessEqual(s, s) + + def test_otherEquality(self): + x = oidutil.Symbol('xxx') + y = oidutil.Symbol('xxx') + self.failUnlessEqual(x, y) + + def test_inequality(self): + x = oidutil.Symbol('xxx') + y = oidutil.Symbol('yyy') + self.failIfEqual(x, y) + + def test_selfInequality(self): + x = oidutil.Symbol('xxx') + self.failIf(x != x) + + def test_otherInequality(self): + x = oidutil.Symbol('xxx') + y = oidutil.Symbol('xxx') + self.failIf(x != y) + + def test_ne_inequality(self): + x = oidutil.Symbol('xxx') + y = oidutil.Symbol('yyy') + self.failUnless(x != y) + +if __name__ == '__main__': + unittest.main() diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_urinorm.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_urinorm.py new file mode 100644 index 0000000..7f5ecaa --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_urinorm.py @@ -0,0 +1,52 @@ +import os +import unittest +import openid.urinorm + +class UrinormTest(unittest.TestCase): + def __init__(self, desc, case, expected): + unittest.TestCase.__init__(self) + self.desc = desc + self.case = case + self.expected = expected + + def shortDescription(self): + return self.desc + + def runTest(self): + try: + actual = openid.urinorm.urinorm(self.case) + except ValueError, why: + self.assertEqual(self.expected, 'fail', why) + else: + self.assertEqual(actual, self.expected) + + def parse(cls, full_case): + desc, case, expected = full_case.split('\n') + case = unicode(case, 'utf-8') + + return cls(desc, case, expected) + + parse = classmethod(parse) + + +def parseTests(test_data): + result = [] + + cases = test_data.split('\n\n') + for case in cases: + case = case.strip() + + if case: + result.append(UrinormTest.parse(case)) + + return result + +def pyUnitTests(): + here = os.path.dirname(os.path.abspath(__file__)) + test_data_file_name = os.path.join(here, 'urinorm.txt') + test_data_file = file(test_data_file_name) + test_data = test_data_file.read() + test_data_file.close() + + tests = parseTests(test_data) + return unittest.TestSuite(tests) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_verifydisco.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_verifydisco.py new file mode 100644 index 0000000..e64a48a --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_verifydisco.py @@ -0,0 +1,270 @@ +import unittest +from openid import message +from openid.test.support import OpenIDTestMixin +from openid.consumer import consumer +from openid.test.test_consumer import TestIdRes +from openid.consumer import discover + +def const(result): + """Return a function that ignores any arguments and just returns + the specified result""" + def constResult(*args, **kwargs): + return result + + return constResult + +class DiscoveryVerificationTest(OpenIDTestMixin, TestIdRes): + def failUnlessProtocolError(self, prefix, callable, *args, **kwargs): + try: + result = callable(*args, **kwargs) + except consumer.ProtocolError, e: + self.failUnless( + e[0].startswith(prefix), + 'Expected message prefix %r, got message %r' % (prefix, e[0])) + else: + self.fail('Expected ProtocolError with prefix %r, ' + 'got successful return %r' % (prefix, result)) + + def test_openID1NoLocalID(self): + endpoint = discover.OpenIDServiceEndpoint() + endpoint.claimed_id = 'bogus' + + msg = message.Message.fromOpenIDArgs({}) + self.failUnlessProtocolError( + 'Missing required field openid.identity', + self.consumer._verifyDiscoveryResults, msg, endpoint) + self.failUnlessLogEmpty() + + def test_openID1NoEndpoint(self): + msg = message.Message.fromOpenIDArgs({'identity':'snakes on a plane'}) + self.failUnlessRaises(RuntimeError, + self.consumer._verifyDiscoveryResults, msg) + self.failUnlessLogEmpty() + + def test_openID2NoOPEndpointArg(self): + msg = message.Message.fromOpenIDArgs({'ns':message.OPENID2_NS}) + self.failUnlessRaises(KeyError, + self.consumer._verifyDiscoveryResults, msg) + self.failUnlessLogEmpty() + + def test_openID2LocalIDNoClaimed(self): + msg = message.Message.fromOpenIDArgs({'ns':message.OPENID2_NS, + 'op_endpoint':'Phone Home', + 'identity':'Jose Lius Borges'}) + self.failUnlessProtocolError( + 'openid.identity is present without', + self.consumer._verifyDiscoveryResults, msg) + self.failUnlessLogEmpty() + + def test_openID2NoLocalIDClaimed(self): + msg = message.Message.fromOpenIDArgs({'ns':message.OPENID2_NS, + 'op_endpoint':'Phone Home', + 'claimed_id':'Manuel Noriega'}) + self.failUnlessProtocolError( + 'openid.claimed_id is present without', + self.consumer._verifyDiscoveryResults, msg) + self.failUnlessLogEmpty() + + def test_openID2NoIdentifiers(self): + op_endpoint = 'Phone Home' + msg = message.Message.fromOpenIDArgs({'ns':message.OPENID2_NS, + 'op_endpoint':op_endpoint}) + result_endpoint = self.consumer._verifyDiscoveryResults(msg) + self.failUnless(result_endpoint.isOPIdentifier()) + self.failUnlessEqual(op_endpoint, result_endpoint.server_url) + self.failUnlessEqual(None, result_endpoint.claimed_id) + self.failUnlessLogEmpty() + + def test_openID2NoEndpointDoesDisco(self): + op_endpoint = 'Phone Home' + sentinel = discover.OpenIDServiceEndpoint() + sentinel.claimed_id = 'monkeysoft' + self.consumer._discoverAndVerify = const(sentinel) + msg = message.Message.fromOpenIDArgs( + {'ns':message.OPENID2_NS, + 'identity':'sour grapes', + 'claimed_id':'monkeysoft', + 'op_endpoint':op_endpoint}) + result = self.consumer._verifyDiscoveryResults(msg) + self.failUnlessEqual(sentinel, result) + self.failUnlessLogMatches('No pre-discovered') + + def test_openID2MismatchedDoesDisco(self): + mismatched = discover.OpenIDServiceEndpoint() + mismatched.identity = 'nothing special, but different' + mismatched.local_id = 'green cheese' + + op_endpoint = 'Phone Home' + sentinel = discover.OpenIDServiceEndpoint() + sentinel.claimed_id = 'monkeysoft' + self.consumer._discoverAndVerify = const(sentinel) + msg = message.Message.fromOpenIDArgs( + {'ns':message.OPENID2_NS, + 'identity':'sour grapes', + 'claimed_id':'monkeysoft', + 'op_endpoint':op_endpoint}) + result = self.consumer._verifyDiscoveryResults(msg, mismatched) + self.failUnlessEqual(sentinel, result) + self.failUnlessLogMatches('Error attempting to use stored', + 'Attempting discovery') + + def test_openid2UsePreDiscovered(self): + endpoint = discover.OpenIDServiceEndpoint() + endpoint.local_id = 'my identity' + endpoint.claimed_id = 'i am sam' + endpoint.server_url = 'Phone Home' + endpoint.type_uris = [discover.OPENID_2_0_TYPE] + + msg = message.Message.fromOpenIDArgs( + {'ns':message.OPENID2_NS, + 'identity':endpoint.local_id, + 'claimed_id':endpoint.claimed_id, + 'op_endpoint':endpoint.server_url}) + result = self.consumer._verifyDiscoveryResults(msg, endpoint) + self.failUnless(result is endpoint) + self.failUnlessLogEmpty() + + def test_openid2UsePreDiscoveredWrongType(self): + text = "verify failed" + + endpoint = discover.OpenIDServiceEndpoint() + endpoint.local_id = 'my identity' + endpoint.claimed_id = 'i am sam' + endpoint.server_url = 'Phone Home' + endpoint.type_uris = [discover.OPENID_1_1_TYPE] + + def discoverAndVerify(claimed_id, to_match_endpoints): + self.failUnlessEqual(claimed_id, endpoint.claimed_id) + for to_match in to_match_endpoints: + self.failUnlessEqual(claimed_id, to_match.claimed_id) + raise consumer.ProtocolError(text) + + self.consumer._discoverAndVerify = discoverAndVerify + + msg = message.Message.fromOpenIDArgs( + {'ns':message.OPENID2_NS, + 'identity':endpoint.local_id, + 'claimed_id':endpoint.claimed_id, + 'op_endpoint':endpoint.server_url}) + + try: + r = self.consumer._verifyDiscoveryResults(msg, endpoint) + except consumer.ProtocolError, e: + # Should we make more ProtocolError subclasses? + self.failUnless(str(e), text) + else: + self.fail("expected ProtocolError, %r returned." % (r,)) + + self.failUnlessLogMatches('Error attempting to use stored', + 'Attempting discovery') + + def test_openid1UsePreDiscovered(self): + endpoint = discover.OpenIDServiceEndpoint() + endpoint.local_id = 'my identity' + endpoint.claimed_id = 'i am sam' + endpoint.server_url = 'Phone Home' + endpoint.type_uris = [discover.OPENID_1_1_TYPE] + + msg = message.Message.fromOpenIDArgs( + {'ns':message.OPENID1_NS, + 'identity':endpoint.local_id}) + result = self.consumer._verifyDiscoveryResults(msg, endpoint) + self.failUnless(result is endpoint) + self.failUnlessLogEmpty() + + def test_openid1UsePreDiscoveredWrongType(self): + class VerifiedError(Exception): pass + + def discoverAndVerify(claimed_id, _to_match): + raise VerifiedError + + self.consumer._discoverAndVerify = discoverAndVerify + + endpoint = discover.OpenIDServiceEndpoint() + endpoint.local_id = 'my identity' + endpoint.claimed_id = 'i am sam' + endpoint.server_url = 'Phone Home' + endpoint.type_uris = [discover.OPENID_2_0_TYPE] + + msg = message.Message.fromOpenIDArgs( + {'ns':message.OPENID1_NS, + 'identity':endpoint.local_id}) + + self.failUnlessRaises( + VerifiedError, + self.consumer._verifyDiscoveryResults, msg, endpoint) + + self.failUnlessLogMatches('Error attempting to use stored', + 'Attempting discovery') + + def test_openid2Fragment(self): + claimed_id = "http://unittest.invalid/" + claimed_id_frag = claimed_id + "#fragment" + endpoint = discover.OpenIDServiceEndpoint() + endpoint.local_id = 'my identity' + endpoint.claimed_id = claimed_id + endpoint.server_url = 'Phone Home' + endpoint.type_uris = [discover.OPENID_2_0_TYPE] + + msg = message.Message.fromOpenIDArgs( + {'ns':message.OPENID2_NS, + 'identity':endpoint.local_id, + 'claimed_id': claimed_id_frag, + 'op_endpoint': endpoint.server_url}) + result = self.consumer._verifyDiscoveryResults(msg, endpoint) + + self.failUnlessEqual(result.local_id, endpoint.local_id) + self.failUnlessEqual(result.server_url, endpoint.server_url) + self.failUnlessEqual(result.type_uris, endpoint.type_uris) + + self.failUnlessEqual(result.claimed_id, claimed_id_frag) + + self.failUnlessLogEmpty() + + def test_openid1Fallback1_0(self): + claimed_id = 'http://claimed.id/' + endpoint = None + resp_mesg = message.Message.fromOpenIDArgs({ + 'ns': message.OPENID1_NS, + 'identity': claimed_id}) + # Pass the OpenID 1 claimed_id this way since we're passing + # None for the endpoint. + resp_mesg.setArg(message.BARE_NS, 'openid1_claimed_id', claimed_id) + + # We expect the OpenID 1 discovery verification to try + # matching the discovered endpoint against the 1.1 type and + # fall back to 1.0. + expected_endpoint = discover.OpenIDServiceEndpoint() + expected_endpoint.type_uris = [discover.OPENID_1_0_TYPE] + expected_endpoint.local_id = None + expected_endpoint.claimed_id = claimed_id + + discovered_services = [expected_endpoint] + self.consumer._discover = lambda *args: ('unused', discovered_services) + + actual_endpoint = self.consumer._verifyDiscoveryResults( + resp_mesg, endpoint) + self.failUnless(actual_endpoint is expected_endpoint) + +# XXX: test the implementation of _discoverAndVerify + + +class TestVerifyDiscoverySingle(TestIdRes): + # XXX: more test the implementation of _verifyDiscoverySingle + def test_endpointWithoutLocalID(self): + # An endpoint like this with no local_id is generated as a result of + # e.g. Yadis discovery with no LocalID tag. + endpoint = discover.OpenIDServiceEndpoint() + endpoint.server_url = "http://localhost:8000/openidserver" + endpoint.claimed_id = "http://localhost:8000/id/id-jo" + to_match = discover.OpenIDServiceEndpoint() + to_match.server_url = "http://localhost:8000/openidserver" + to_match.claimed_id = "http://localhost:8000/id/id-jo" + to_match.local_id = "http://localhost:8000/id/id-jo" + result = self.consumer._verifyDiscoverySingle(endpoint, to_match) + # result should always be None, raises exception on failure. + self.failUnlessEqual(result, None) + self.failUnlessLogEmpty() + +if __name__ == '__main__': + unittest.main() diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_xri.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_xri.py new file mode 100644 index 0000000..dc5921a --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_xri.py @@ -0,0 +1,102 @@ +from unittest import TestCase +from openid.yadis import xri + +class XriDiscoveryTestCase(TestCase): + def test_isXRI(self): + i = xri.identifierScheme + self.failUnlessEqual(i('=john.smith'), 'XRI') + self.failUnlessEqual(i('@smiths/john'), 'XRI') + self.failUnlessEqual(i('smoker.myopenid.com'), 'URI') + self.failUnlessEqual(i('xri://=john'), 'XRI') + self.failUnlessEqual(i(''), 'URI') + + +class XriEscapingTestCase(TestCase): + def test_escaping_percents(self): + self.failUnlessEqual(xri.escapeForIRI('@example/abc%2Fd/ef'), + '@example/abc%252Fd/ef') + + + def test_escaping_xref(self): + # no escapes + esc = xri.escapeForIRI + self.failUnlessEqual('@example/foo/(@bar)', esc('@example/foo/(@bar)')) + # escape slashes + self.failUnlessEqual('@example/foo/(@bar%2Fbaz)', + esc('@example/foo/(@bar/baz)')) + self.failUnlessEqual('@example/foo/(@bar%2Fbaz)/(+a%2Fb)', + esc('@example/foo/(@bar/baz)/(+a/b)')) + # escape query ? and fragment # + self.failUnlessEqual('@example/foo/(@baz%3Fp=q%23r)?i=j#k', + esc('@example/foo/(@baz?p=q#r)?i=j#k')) + + + +class XriTransformationTestCase(TestCase): + def test_to_iri_normal(self): + self.failUnlessEqual(xri.toIRINormal('@example'), 'xri://@example') + + try: + unichr(0x10000) + except ValueError: + # bleh narrow python build + def test_iri_to_url(self): + s = u'l\xa1m' + expected = 'l%C2%A1m' + self.failUnlessEqual(xri.iriToURI(s), expected) + else: + def test_iri_to_url(self): + s = u'l\xa1m\U00101010n' + expected = 'l%C2%A1m%F4%81%80%90n' + self.failUnlessEqual(xri.iriToURI(s), expected) + + + +class CanonicalIDTest(TestCase): + def mkTest(providerID, canonicalID, isAuthoritative): + def test(self): + result = xri.providerIsAuthoritative(providerID, canonicalID) + format = "%s providing %s, expected %s" + message = format % (providerID, canonicalID, isAuthoritative) + self.failUnlessEqual(isAuthoritative, result, message) + + return test + + test_equals = mkTest('=', '=!698.74D1.A1F2.86C7', True) + test_atOne = mkTest('@!1234', '@!1234!ABCD', True) + test_atTwo = mkTest('@!1234!5678', '@!1234!5678!ABCD', True) + + test_atEqualsFails = mkTest('@!1234', '=!1234!ABCD', False) + test_tooDeepFails = mkTest('@!1234', '@!1234!ABCD!9765', False) + test_atEqualsAndTooDeepFails = mkTest('@!1234!ABCD', '=!1234', False) + test_differentBeginningFails = mkTest('=!BABE', '=!D00D', False) + +class TestGetRootAuthority(TestCase): + def mkTest(the_xri, expected_root): + def test(self): + actual_root = xri.rootAuthority(the_xri) + self.failUnlessEqual(actual_root, xri.XRI(expected_root)) + return test + + test_at = mkTest("@foo", "@") + test_atStar = mkTest("@foo*bar", "@") + test_atStarStar = mkTest("@*foo*bar", "@") + test_atWithPath = mkTest("@foo/bar", "@") + test_bangBang = mkTest("!!990!991", "!") + test_bang = mkTest("!1001!02", "!") + test_equalsStar = mkTest("=foo*bar", "=") + test_xrefPath = mkTest("(example.com)/foo", "(example.com)") + test_xrefStar = mkTest("(example.com)*bar/foo", "(example.com)") + test_uriAuth = mkTest("baz.example.com/foo", "baz.example.com") + test_uriAuthPort = mkTest("baz.example.com:8080/foo", + "baz.example.com:8080") + + # Looking at the ABNF in XRI Syntax 2.0, I don't think you can + # have example.com*bar. You can do (example.com)*bar, but that + # would mean something else. + ##("example.com*bar/(=baz)", "example.com*bar"), + ##("baz.example.com!01/foo", "baz.example.com!01"), + +if __name__ == '__main__': + import unittest + unittest.main() diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_xrires.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_xrires.py new file mode 100644 index 0000000..4e17e3b --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_xrires.py @@ -0,0 +1,40 @@ + +from unittest import TestCase +from openid.yadis import xrires + +class ProxyQueryTestCase(TestCase): + def setUp(self): + self.proxy_url = 'http://xri.example.com/' + self.proxy = xrires.ProxyResolver(self.proxy_url) + self.servicetype = 'xri://+i-service*(+forwarding)*($v*1.0)' + self.servicetype_enc = 'xri%3A%2F%2F%2Bi-service%2A%28%2Bforwarding%29%2A%28%24v%2A1.0%29' + + + def test_proxy_url(self): + st = self.servicetype + ste = self.servicetype_enc + args_esc = "_xrd_r=application%2Fxrds%2Bxml&_xrd_t=" + ste + pqu = self.proxy.queryURL + h = self.proxy_url + self.failUnlessEqual(h + '=foo?' + args_esc, pqu('=foo', st)) + self.failUnlessEqual(h + '=foo/bar?baz&' + args_esc, + pqu('=foo/bar?baz', st)) + self.failUnlessEqual(h + '=foo/bar?baz=quux&' + args_esc, + pqu('=foo/bar?baz=quux', st)) + self.failUnlessEqual(h + '=foo/bar?mi=fa&so=la&' + args_esc, + pqu('=foo/bar?mi=fa&so=la', st)) + + # With no service endpoint selection. + args_esc = "_xrd_r=application%2Fxrds%2Bxml%3Bsep%3Dfalse" + self.failUnlessEqual(h + '=foo?' + args_esc, pqu('=foo', None)) + + + def test_proxy_url_qmarks(self): + st = self.servicetype + ste = self.servicetype_enc + args_esc = "_xrd_r=application%2Fxrds%2Bxml&_xrd_t=" + ste + pqu = self.proxy.queryURL + h = self.proxy_url + self.failUnlessEqual(h + '=foo/bar??' + args_esc, pqu('=foo/bar?', st)) + self.failUnlessEqual(h + '=foo/bar????' + args_esc, + pqu('=foo/bar???', st)) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_yadis_discover.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_yadis_discover.py new file mode 100644 index 0000000..8a7677f --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/test_yadis_discover.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python + +"""Tests for yadis.discover. + +@todo: Now that yadis.discover uses urljr.fetchers, we should be able to do + tests with a mock fetcher instead of spawning threads with BaseHTTPServer. +""" + +import unittest +import urlparse +import re +import types + +from openid.yadis.discover import discover, DiscoveryFailure + +from openid import fetchers + +import discoverdata + +status_header_re = re.compile(r'Status: (\d+) .*?$', re.MULTILINE) + +four04_pat = """\ +Content-Type: text/plain + +No such file %s +""" + +class QuitServer(Exception): pass + +def mkResponse(data): + status_mo = status_header_re.match(data) + headers_str, body = data.split('\n\n', 1) + headers = {} + for line in headers_str.split('\n'): + k, v = line.split(':', 1) + k = k.strip().lower() + v = v.strip() + headers[k] = v + status = int(status_mo.group(1)) + return fetchers.HTTPResponse(status=status, + headers=headers, + body=body) + +class TestFetcher(object): + def __init__(self, base_url): + self.base_url = base_url + + def fetch(self, url, headers, body): + current_url = url + while True: + parsed = urlparse.urlparse(current_url) + path = parsed[2][1:] + try: + data = discoverdata.generateSample(path, self.base_url) + except KeyError: + return fetchers.HTTPResponse(status=404, + final_url=current_url, + headers={}, + body='') + + response = mkResponse(data) + if response.status in [301, 302, 303, 307]: + current_url = response.headers['location'] + else: + response.final_url = current_url + return response + +class TestSecondGet(unittest.TestCase): + class MockFetcher(object): + def __init__(self): + self.count = 0 + def fetch(self, uri, headers=None, body=None): + self.count += 1 + if self.count == 1: + headers = { + 'X-XRDS-Location'.lower(): 'http://unittest/404', + } + return fetchers.HTTPResponse(uri, 200, headers, '') + else: + return fetchers.HTTPResponse(uri, 404) + + def setUp(self): + self.oldfetcher = fetchers.getDefaultFetcher() + fetchers.setDefaultFetcher(self.MockFetcher()) + + def tearDown(self): + fetchers.setDefaultFetcher(self.oldfetcher) + + def test_404(self): + uri = "http://something.unittest/" + self.failUnlessRaises(DiscoveryFailure, discover, uri) + + +class _TestCase(unittest.TestCase): + base_url = 'http://invalid.unittest/' + + def __init__(self, input_name, id_name, result_name, success): + self.input_name = input_name + self.id_name = id_name + self.result_name = result_name + self.success = success + # Still not quite sure how to best construct these custom tests. + # Between python2.3 and python2.4, a patch attached to pyunit.sf.net + # bug #469444 got applied which breaks loadTestsFromModule on this + # class if it has test_ or runTest methods. So, kludge to change + # the method name. + unittest.TestCase.__init__(self, methodName='runCustomTest') + + def setUp(self): + fetchers.setDefaultFetcher(TestFetcher(self.base_url), + wrap_exceptions=False) + + self.input_url, self.expected = discoverdata.generateResult( + self.base_url, + self.input_name, + self.id_name, + self.result_name, + self.success) + + def tearDown(self): + fetchers.setDefaultFetcher(None) + + def runCustomTest(self): + if self.expected is DiscoveryFailure: + self.failUnlessRaises(DiscoveryFailure, + discover, self.input_url) + else: + result = discover(self.input_url) + self.failUnlessEqual(self.input_url, result.request_uri) + + msg = 'Identity URL mismatch: actual = %r, expected = %r' % ( + result.normalized_uri, self.expected.normalized_uri) + self.failUnlessEqual( + self.expected.normalized_uri, result.normalized_uri, msg) + + msg = 'Content mismatch: actual = %r, expected = %r' % ( + result.response_text, self.expected.response_text) + self.failUnlessEqual( + self.expected.response_text, result.response_text, msg) + + expected_keys = dir(self.expected) + expected_keys.sort() + actual_keys = dir(result) + actual_keys.sort() + self.failUnlessEqual(actual_keys, expected_keys) + + for k in dir(self.expected): + if k.startswith('__') and k.endswith('__'): + continue + exp_v = getattr(self.expected, k) + if isinstance(exp_v, types.MethodType): + continue + act_v = getattr(result, k) + assert act_v == exp_v, (k, exp_v, act_v) + + def shortDescription(self): + try: + n = self.input_url + except AttributeError: + # run before setUp, or if setUp did not complete successfully. + n = self.input_name + return "%s (%s)" % ( + n, + self.__class__.__module__) + +def pyUnitTests(): + s = unittest.TestSuite() + for success, input_name, id_name, result_name in discoverdata.testlist: + test = _TestCase(input_name, id_name, result_name, success) + s.addTest(test) + + return s + +def test(): + runner = unittest.TextTestRunner() + return runner.run(loadTests()) + +if __name__ == '__main__': + test() diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/trustroot.py b/desktop/core/ext-py/python-openid-2.2.5/openid/test/trustroot.py new file mode 100644 index 0000000..236649b --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/trustroot.py @@ -0,0 +1,85 @@ +import os +import unittest +from openid.server.trustroot import TrustRoot + +class _ParseTest(unittest.TestCase): + def __init__(self, sanity, desc, case): + unittest.TestCase.__init__(self) + self.desc = desc + ': ' + repr(case) + self.case = case + self.sanity = sanity + + def shortDescription(self): + return self.desc + + def runTest(self): + tr = TrustRoot.parse(self.case) + if self.sanity == 'sane': + assert tr.isSane(), self.case + elif self.sanity == 'insane': + assert not tr.isSane(), self.case + else: + assert tr is None, tr + +class _MatchTest(unittest.TestCase): + def __init__(self, match, desc, line): + unittest.TestCase.__init__(self) + tr, rt = line.split() + self.desc = desc + ': ' + repr(tr) + ' ' + repr(rt) + self.tr = tr + self.rt = rt + self.match = match + + def shortDescription(self): + return self.desc + + def runTest(self): + tr = TrustRoot.parse(self.tr) + self.failIf(tr is None, self.tr) + + match = tr.validateURL(self.rt) + if self.match: + assert match + else: + assert not match + +def getTests(t, grps, head, dat): + tests = [] + top = head.strip() + gdat = map(str.strip, dat.split('-' * 40 + '\n')) + assert not gdat[0] + assert len(gdat) == (len(grps) * 2 + 1), (gdat, grps) + i = 1 + for x in grps: + n, desc = gdat[i].split(': ') + cases = gdat[i + 1].split('\n') + assert len(cases) == int(n) + for case in cases: + tests.append(t(x, top + ' - ' + desc, case)) + i += 2 + return tests + +def parseTests(data): + parts = map(str.strip, data.split('=' * 40 + '\n')) + assert not parts[0] + _, ph, pdat, mh, mdat = parts + + tests = [] + tests.extend(getTests(_ParseTest, ['bad', 'insane', 'sane'], ph, pdat)) + tests.extend(getTests(_MatchTest, [1, 0], mh, mdat)) + return tests + +def pyUnitTests(): + here = os.path.dirname(os.path.abspath(__file__)) + test_data_file_name = os.path.join(here, 'data', 'trustroot.txt') + test_data_file = file(test_data_file_name) + test_data = test_data_file.read() + test_data_file.close() + + tests = parseTests(test_data) + return unittest.TestSuite(tests) + +if __name__ == '__main__': + suite = pyUnitTests() + runner = unittest.TextTestRunner() + runner.run(suite) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/test/urinorm.txt b/desktop/core/ext-py/python-openid-2.2.5/openid/test/urinorm.txt new file mode 100644 index 0000000..a5db39e --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/test/urinorm.txt @@ -0,0 +1,87 @@ +Already normal form +http://example.com/ +http://example.com/ + +Add a trailing slash +http://example.com +http://example.com/ + +Remove an empty port segment +http://example.com:/ +http://example.com/ + +Remove a default port segment +http://example.com:80/ +http://example.com/ + +Capitalization in host names +http://wWw.exaMPLE.COm/ +http://www.example.com/ + +Capitalization in scheme names +htTP://example.com/ +http://example.com/ + +Capitalization in percent-escaped reserved characters +http://example.com/foo%2cbar +http://example.com/foo%2Cbar + +Unescape percent-encoded unreserved characters +http://example.com/foo%2Dbar%2dbaz +http://example.com/foo-bar-baz + +remove_dot_segments example 1 +http://example.com/a/b/c/./../../g +http://example.com/a/g + +remove_dot_segments example 2 +http://example.com/mid/content=5/../6 +http://example.com/mid/6 + +remove_dot_segments: single-dot +http://example.com/a/./b +http://example.com/a/b + +remove_dot_segments: double-dot +http://example.com/a/../b +http://example.com/b + +remove_dot_segments: leading double-dot +http://example.com/../b +http://example.com/b + +remove_dot_segments: trailing single-dot +http://example.com/a/. +http://example.com/a/ + +remove_dot_segments: trailing double-dot +http://example.com/a/.. +http://example.com/ + +remove_dot_segments: trailing single-dot-slash +http://example.com/a/./ +http://example.com/a/ + +remove_dot_segments: trailing double-dot-slash +http://example.com/a/../ +http://example.com/ + +Test of all kinds of syntax-based normalization +hTTPS://a/./b/../b/%63/%7bfoo%7d +https://a/b/c/%7Bfoo%7D + +Unsupported scheme +ftp://example.com/ +fail + +Non-absolute URI +http:/foo +fail + +Illegal character in URI +http://.com/ +fail + +Non-ascii character in URI +http://foo.com/ +fail diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/urinorm.py b/desktop/core/ext-py/python-openid-2.2.5/openid/urinorm.py new file mode 100644 index 0000000..5bdbaef --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/urinorm.py @@ -0,0 +1,202 @@ +import re + +# from appendix B of rfc 3986 (http://www.ietf.org/rfc/rfc3986.txt) +uri_pattern = r'^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?' +uri_re = re.compile(uri_pattern) + +# gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" +# +# sub-delims = "!" / "$" / "&" / "'" / "(" / ")" +# / "*" / "+" / "," / ";" / "=" +# +# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + +uri_illegal_char_re = re.compile( + "[^-A-Za-z0-9:/?#[\]@!$&'()*+,;=._~%]", re.UNICODE) + +authority_pattern = r'^([^@]*@)?([^:]*)(:.*)?' +authority_re = re.compile(authority_pattern) + + +pct_encoded_pattern = r'%([0-9A-Fa-f]{2})' +pct_encoded_re = re.compile(pct_encoded_pattern) + +try: + unichr(0x10000) +except ValueError: + # narrow python build + UCSCHAR = [ + (0xA0, 0xD7FF), + (0xF900, 0xFDCF), + (0xFDF0, 0xFFEF), + ] + + IPRIVATE = [ + (0xE000, 0xF8FF), + ] +else: + UCSCHAR = [ + (0xA0, 0xD7FF), + (0xF900, 0xFDCF), + (0xFDF0, 0xFFEF), + (0x10000, 0x1FFFD), + (0x20000, 0x2FFFD), + (0x30000, 0x3FFFD), + (0x40000, 0x4FFFD), + (0x50000, 0x5FFFD), + (0x60000, 0x6FFFD), + (0x70000, 0x7FFFD), + (0x80000, 0x8FFFD), + (0x90000, 0x9FFFD), + (0xA0000, 0xAFFFD), + (0xB0000, 0xBFFFD), + (0xC0000, 0xCFFFD), + (0xD0000, 0xDFFFD), + (0xE1000, 0xEFFFD), + ] + + IPRIVATE = [ + (0xE000, 0xF8FF), + (0xF0000, 0xFFFFD), + (0x100000, 0x10FFFD), + ] + + +_unreserved = [False] * 256 +for _ in range(ord('A'), ord('Z') + 1): _unreserved[_] = True +for _ in range(ord('0'), ord('9') + 1): _unreserved[_] = True +for _ in range(ord('a'), ord('z') + 1): _unreserved[_] = True +_unreserved[ord('-')] = True +_unreserved[ord('.')] = True +_unreserved[ord('_')] = True +_unreserved[ord('~')] = True + + +_escapeme_re = re.compile('[%s]' % (''.join( + map(lambda (m, n): u'%s-%s' % (unichr(m), unichr(n)), + UCSCHAR + IPRIVATE)),)) + + +def _pct_escape_unicode(char_match): + c = char_match.group() + return ''.join(['%%%X' % (ord(octet),) for octet in c.encode('utf-8')]) + + +def _pct_encoded_replace_unreserved(mo): + try: + i = int(mo.group(1), 16) + if _unreserved[i]: + return chr(i) + else: + return mo.group().upper() + + except ValueError: + return mo.group() + + +def _pct_encoded_replace(mo): + try: + return chr(int(mo.group(1), 16)) + except ValueError: + return mo.group() + + +def remove_dot_segments(path): + result_segments = [] + + while path: + if path.startswith('../'): + path = path[3:] + elif path.startswith('./'): + path = path[2:] + elif path.startswith('/./'): + path = path[2:] + elif path == '/.': + path = '/' + elif path.startswith('/../'): + path = path[3:] + if result_segments: + result_segments.pop() + elif path == '/..': + path = '/' + if result_segments: + result_segments.pop() + elif path == '..' or path == '.': + path = '' + else: + i = 0 + if path[0] == '/': + i = 1 + i = path.find('/', i) + if i == -1: + i = len(path) + result_segments.append(path[:i]) + path = path[i:] + + return ''.join(result_segments) + + +def urinorm(uri): + if isinstance(uri, unicode): + uri = _escapeme_re.sub(_pct_escape_unicode, uri).encode('ascii') + + illegal_mo = uri_illegal_char_re.search(uri) + if illegal_mo: + raise ValueError('Illegal characters in URI: %r at position %s' % + (illegal_mo.group(), illegal_mo.start())) + + uri_mo = uri_re.match(uri) + + scheme = uri_mo.group(2) + if scheme is None: + raise ValueError('No scheme specified') + + scheme = scheme.lower() + if scheme not in ('http', 'https'): + raise ValueError('Not an absolute HTTP or HTTPS URI: %r' % (uri,)) + + authority = uri_mo.group(4) + if authority is None: + raise ValueError('Not an absolute URI: %r' % (uri,)) + + authority_mo = authority_re.match(authority) + if authority_mo is None: + raise ValueError('URI does not have a valid authority: %r' % (uri,)) + + userinfo, host, port = authority_mo.groups() + + if userinfo is None: + userinfo = '' + + if '%' in host: + host = host.lower() + host = pct_encoded_re.sub(_pct_encoded_replace, host) + host = unicode(host, 'utf-8').encode('idna') + else: + host = host.lower() + + if port: + if (port == ':' or + (scheme == 'http' and port == ':80') or + (scheme == 'https' and port == ':443')): + port = '' + else: + port = '' + + authority = userinfo + host + port + + path = uri_mo.group(5) + path = pct_encoded_re.sub(_pct_encoded_replace_unreserved, path) + path = remove_dot_segments(path) + if not path: + path = '/' + + query = uri_mo.group(6) + if query is None: + query = '' + + fragment = uri_mo.group(8) + if fragment is None: + fragment = '' + + return scheme + '://' + authority + path + query + fragment diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/__init__.py b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/__init__.py new file mode 100644 index 0000000..cfa5f1e --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/__init__.py @@ -0,0 +1,26 @@ + + +__all__ = [ + 'constants', + 'discover', + 'etxrd', + 'filters', + 'manager', + 'parsehtml', + 'services', + 'xri', + 'xrires', + ] + +__version__ = '[library version:1.1.0-rc1]'[17:-1] + +# Parse the version info +try: + version_info = map(int, __version__.split('.')) +except ValueError: + version_info = (None, None, None) +else: + if len(version_info) != 3: + version_info = (None, None, None) + else: + version_info = tuple(version_info) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/accept.py b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/accept.py new file mode 100644 index 0000000..d750813 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/accept.py @@ -0,0 +1,133 @@ +"""Functions for generating and parsing HTTP Accept: headers for +supporting server-directed content negotiation. +""" + +def generateAcceptHeader(*elements): + """Generate an accept header value + + [str or (str, float)] -> str + """ + parts = [] + for element in elements: + if type(element) is str: + qs = "1.0" + mtype = element + else: + mtype, q = element + q = float(q) + if q > 1 or q <= 0: + raise ValueError('Invalid preference factor: %r' % q) + + qs = '%0.1f' % (q,) + + parts.append((qs, mtype)) + + parts.sort() + chunks = [] + for q, mtype in parts: + if q == '1.0': + chunks.append(mtype) + else: + chunks.append('%s; q=%s' % (mtype, q)) + + return ', '.join(chunks) + +def parseAcceptHeader(value): + """Parse an accept header, ignoring any accept-extensions + + returns a list of tuples containing main MIME type, MIME subtype, + and quality markdown. + + str -> [(str, str, float)] + """ + chunks = [chunk.strip() for chunk in value.split(',')] + accept = [] + for chunk in chunks: + parts = [s.strip() for s in chunk.split(';')] + + mtype = parts.pop(0) + if '/' not in mtype: + # This is not a MIME type, so ignore the bad data + continue + + main, sub = mtype.split('/', 1) + + for ext in parts: + if '=' in ext: + k, v = ext.split('=', 1) + if k == 'q': + try: + q = float(v) + break + except ValueError: + # Ignore poorly formed q-values + pass + else: + q = 1.0 + + accept.append((q, main, sub)) + + accept.sort() + accept.reverse() + return [(main, sub, q) for (q, main, sub) in accept] + +def matchTypes(accept_types, have_types): + """Given the result of parsing an Accept: header, and the + available MIME types, return the acceptable types with their + quality markdowns. + + For example: + + >>> acceptable = parseAcceptHeader('text/html, text/plain; q=0.5') + >>> matchTypes(acceptable, ['text/plain', 'text/html', 'image/jpeg']) + [('text/html', 1.0), ('text/plain', 0.5)] + + + Type signature: ([(str, str, float)], [str]) -> [(str, float)] + """ + if not accept_types: + # Accept all of them + default = 1 + else: + default = 0 + + match_main = {} + match_sub = {} + for (main, sub, q) in accept_types: + if main == '*': + default = max(default, q) + continue + elif sub == '*': + match_main[main] = max(match_main.get(main, 0), q) + else: + match_sub[(main, sub)] = max(match_sub.get((main, sub), 0), q) + + accepted_list = [] + order_maintainer = 0 + for mtype in have_types: + main, sub = mtype.split('/') + if (main, sub) in match_sub: + q = match_sub[(main, sub)] + else: + q = match_main.get(main, default) + + if q: + accepted_list.append((1 - q, order_maintainer, q, mtype)) + order_maintainer += 1 + + accepted_list.sort() + return [(mtype, q) for (_, _, q, mtype) in accepted_list] + +def getAcceptable(accept_header, have_types): + """Parse the accept header and return a list of available types in + preferred order. If a type is unacceptable, it will not be in the + resulting list. + + This is a convenience wrapper around matchTypes and + parseAcceptHeader. + + (str, [str]) -> [str] + """ + accepted = parseAcceptHeader(accept_header) + preferred = matchTypes(accepted, have_types) + return [mtype for (mtype, _) in preferred] diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/constants.py b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/constants.py new file mode 100644 index 0000000..75ff96e --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/constants.py @@ -0,0 +1,13 @@ +__all__ = ['YADIS_HEADER_NAME', 'YADIS_CONTENT_TYPE', 'YADIS_ACCEPT_HEADER'] +from openid.yadis.accept import generateAcceptHeader + +YADIS_HEADER_NAME = 'X-XRDS-Location' +YADIS_CONTENT_TYPE = 'application/xrds+xml' + +# A value suitable for using as an accept header when performing YADIS +# discovery, unless the application has special requirements +YADIS_ACCEPT_HEADER = generateAcceptHeader( + ('text/html', 0.3), + ('application/xhtml+xml', 0.5), + (YADIS_CONTENT_TYPE, 1.0), + ) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/discover.py b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/discover.py new file mode 100644 index 0000000..ed2adfd --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/discover.py @@ -0,0 +1,135 @@ +# -*- test-case-name: openid.test.test_yadis_discover -*- +__all__ = ['discover', 'DiscoveryResult', 'DiscoveryFailure'] + +from cStringIO import StringIO + +from openid import fetchers + +from openid.yadis.constants import \ + YADIS_HEADER_NAME, YADIS_CONTENT_TYPE, YADIS_ACCEPT_HEADER +from openid.yadis.parsehtml import MetaNotFound, findHTMLMeta + +class DiscoveryFailure(Exception): + """Raised when a YADIS protocol error occurs in the discovery process""" + identity_url = None + + def __init__(self, message, http_response): + Exception.__init__(self, message) + self.http_response = http_response + +class DiscoveryResult(object): + """Contains the result of performing Yadis discovery on a URI""" + + # The URI that was passed to the fetcher + request_uri = None + + # The result of following redirects from the request_uri + normalized_uri = None + + # The URI from which the response text was returned (set to + # None if there was no XRDS document found) + xrds_uri = None + + # The content-type returned with the response_text + content_type = None + + # The document returned from the xrds_uri + response_text = None + + def __init__(self, request_uri): + """Initialize the state of the object + + sets all attributes to None except the request_uri + """ + self.request_uri = request_uri + + def usedYadisLocation(self): + """Was the Yadis protocol's indirection used?""" + return self.normalized_uri != self.xrds_uri + + def isXRDS(self): + """Is the response text supposed to be an XRDS document?""" + return (self.usedYadisLocation() or + self.content_type == YADIS_CONTENT_TYPE) + +def discover(uri): + """Discover services for a given URI. + + @param uri: The identity URI as a well-formed http or https + URI. The well-formedness and the protocol are not checked, but + the results of this function are undefined if those properties + do not hold. + + @return: DiscoveryResult object + + @raises Exception: Any exception that can be raised by fetching a URL with + the given fetcher. + @raises DiscoveryFailure: When the HTTP response does not have a 200 code. + """ + result = DiscoveryResult(uri) + resp = fetchers.fetch(uri, headers={'Accept': YADIS_ACCEPT_HEADER}) + if resp.status not in (200, 206): + raise DiscoveryFailure( + 'HTTP Response status from identity URL host is not 200. ' + 'Got status %r' % (resp.status,), resp) + + # Note the URL after following redirects + result.normalized_uri = resp.final_url + + # Attempt to find out where to go to discover the document + # or if we already have it + result.content_type = resp.headers.get('content-type') + + result.xrds_uri = whereIsYadis(resp) + + if result.xrds_uri and result.usedYadisLocation(): + resp = fetchers.fetch(result.xrds_uri) + if resp.status not in (200, 206): + exc = DiscoveryFailure( + 'HTTP Response status from Yadis host is not 200. ' + 'Got status %r' % (resp.status,), resp) + exc.identity_url = result.normalized_uri + raise exc + result.content_type = resp.headers.get('content-type') + + result.response_text = resp.body + return result + + + +def whereIsYadis(resp): + """Given a HTTPResponse, return the location of the Yadis document. + + May be the URL just retrieved, another URL, or None, if I can't + find any. + + [non-blocking] + + @returns: str or None + """ + # Attempt to find out where to go to discover the document + # or if we already have it + content_type = resp.headers.get('content-type') + + # According to the spec, the content-type header must be an exact + # match, or else we have to look for an indirection. + if (content_type and + content_type.split(';', 1)[0].lower() == YADIS_CONTENT_TYPE): + return resp.final_url + else: + # Try the header + yadis_loc = resp.headers.get(YADIS_HEADER_NAME.lower()) + + if not yadis_loc: + # Parse as HTML if the header is missing. + # + # XXX: do we want to do something with content-type, like + # have a whitelist or a blacklist (for detecting that it's + # HTML)? + try: + yadis_loc = findHTMLMeta(StringIO(resp.body)) + except MetaNotFound: + pass + + return yadis_loc + diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/etxrd.py b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/etxrd.py new file mode 100644 index 0000000..ef5cadf --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/etxrd.py @@ -0,0 +1,300 @@ +# -*- test-case-name: yadis.test.test_etxrd -*- +""" +ElementTree interface to an XRD document. +""" + +__all__ = [ + 'nsTag', + 'mkXRDTag', + 'isXRDS', + 'parseXRDS', + 'getCanonicalID', + 'getYadisXRD', + 'getPriorityStrict', + 'getPriority', + 'prioSort', + 'iterServices', + 'expandService', + 'expandServices', + ] + +import sys +import random + +from datetime import datetime +from time import strptime + +from openid.oidutil import importElementTree +ElementTree = importElementTree() + +# the different elementtree modules don't have a common exception +# model. We just want to be able to catch the exceptions that signify +# malformed XML data and wrap them, so that the other library code +# doesn't have to know which XML library we're using. +try: + # Make the parser raise an exception so we can sniff out the type + # of exceptions + ElementTree.XML('> purposely malformed XML <') +except (SystemExit, MemoryError, AssertionError, ImportError): + raise +except: + XMLError = sys.exc_info()[0] + +from openid.yadis import xri + +class XRDSError(Exception): + """An error with the XRDS document.""" + + # The exception that triggered this exception + reason = None + + + +class XRDSFraud(XRDSError): + """Raised when there's an assertion in the XRDS that it does not have + the authority to make. + """ + + + +def parseXRDS(text): + """Parse the given text as an XRDS document. + + @return: ElementTree containing an XRDS document + + @raises XRDSError: When there is a parse error or the document does + not contain an XRDS. + """ + try: + element = ElementTree.XML(text) + except XMLError, why: + exc = XRDSError('Error parsing document as XML') + exc.reason = why + raise exc + else: + tree = ElementTree.ElementTree(element) + if not isXRDS(tree): + raise XRDSError('Not an XRDS document') + + return tree + +XRD_NS_2_0 = 'xri://$xrd*($v*2.0)' +XRDS_NS = 'xri://$xrds' + +def nsTag(ns, t): + return '{%s}%s' % (ns, t) + +def mkXRDTag(t): + """basestring -> basestring + + Create a tag name in the XRD 2.0 XML namespace suitable for using + with ElementTree + """ + return nsTag(XRD_NS_2_0, t) + +def mkXRDSTag(t): + """basestring -> basestring + + Create a tag name in the XRDS XML namespace suitable for using + with ElementTree + """ + return nsTag(XRDS_NS, t) + +# Tags that are used in Yadis documents +root_tag = mkXRDSTag('XRDS') +service_tag = mkXRDTag('Service') +xrd_tag = mkXRDTag('XRD') +type_tag = mkXRDTag('Type') +uri_tag = mkXRDTag('URI') +expires_tag = mkXRDTag('Expires') + +# Other XRD tags +canonicalID_tag = mkXRDTag('CanonicalID') + +def isXRDS(xrd_tree): + """Is this document an XRDS document?""" + root = xrd_tree.getroot() + return root.tag == root_tag + +def getYadisXRD(xrd_tree): + """Return the XRD element that should contain the Yadis services""" + xrd = None + + # for the side-effect of assigning the last one in the list to the + # xrd variable + for xrd in xrd_tree.findall(xrd_tag): + pass + + # There were no elements found, or else xrd would be set to the + # last one + if xrd is None: + raise XRDSError('No XRD present in tree') + + return xrd + +def getXRDExpiration(xrd_element, default=None): + """Return the expiration date of this XRD element, or None if no + expiration was specified. + + @type xrd_element: ElementTree node + + @param default: The value to use as the expiration if no + expiration was specified in the XRD. + + @rtype: datetime.datetime + + @raises ValueError: If the xrd:Expires element is present, but its + contents are not formatted according to the specification. + """ + expires_element = xrd_element.find(expires_tag) + if expires_element is None: + return default + else: + expires_string = expires_element.text + + # Will raise ValueError if the string is not the expected format + expires_time = strptime(expires_string, "%Y-%m-%dT%H:%M:%SZ") + return datetime(*expires_time[0:6]) + +def getCanonicalID(iname, xrd_tree): + """Return the CanonicalID from this XRDS document. + + @param iname: the XRI being resolved. + @type iname: unicode + + @param xrd_tree: The XRDS output from the resolver. + @type xrd_tree: ElementTree + + @returns: The XRI CanonicalID or None. + @returntype: unicode or None + """ + xrd_list = xrd_tree.findall(xrd_tag) + xrd_list.reverse() + + try: + canonicalID = xri.XRI(xrd_list[0].findall(canonicalID_tag)[0].text) + except IndexError: + return None + + childID = canonicalID.lower() + + for xrd in xrd_list[1:]: + # XXX: can't use rsplit until we require python >= 2.4. + parent_sought = childID[:childID.rindex('!')] + parent = xri.XRI(xrd.findtext(canonicalID_tag)) + if parent_sought != parent.lower(): + raise XRDSFraud("%r can not come from %s" % (childID, parent)) + + childID = parent_sought + + root = xri.rootAuthority(iname) + if not xri.providerIsAuthoritative(root, childID): + raise XRDSFraud("%r can not come from root %r" % (childID, root)) + + return canonicalID + + + +class _Max(object): + """Value that compares greater than any other value. + + Should only be used as a singleton. Implemented for use as a + priority value for when a priority is not specified.""" + def __cmp__(self, other): + if other is self: + return 0 + + return 1 + +Max = _Max() + +def getPriorityStrict(element): + """Get the priority of this element. + + Raises ValueError if the value of the priority is invalid. If no + priority is specified, it returns a value that compares greater + than any other value. + """ + prio_str = element.get('priority') + if prio_str is not None: + prio_val = int(prio_str) + if prio_val >= 0: + return prio_val + else: + raise ValueError('Priority values must be non-negative integers') + + # Any errors in parsing the priority fall through to here + return Max + +def getPriority(element): + """Get the priority of this element + + Returns Max if no priority is specified or the priority value is invalid. + """ + try: + return getPriorityStrict(element) + except ValueError: + return Max + +def prioSort(elements): + """Sort a list of elements that have priority attributes""" + # Randomize the services before sorting so that equal priority + # elements are load-balanced. + random.shuffle(elements) + + prio_elems = [(getPriority(e), e) for e in elements] + prio_elems.sort() + sorted_elems = [s for (_, s) in prio_elems] + return sorted_elems + +def iterServices(xrd_tree): + """Return an iterable over the Service elements in the Yadis XRD + + sorted by priority""" + xrd = getYadisXRD(xrd_tree) + return prioSort(xrd.findall(service_tag)) + +def sortedURIs(service_element): + """Given a Service element, return a list of the contents of all + URI tags in priority order.""" + return [uri_element.text for uri_element + in prioSort(service_element.findall(uri_tag))] + +def getTypeURIs(service_element): + """Given a Service element, return a list of the contents of all + Type tags""" + return [type_element.text for type_element + in service_element.findall(type_tag)] + +def expandService(service_element): + """Take a service element and expand it into an iterator of: + ([type_uri], uri, service_element) + """ + uris = sortedURIs(service_element) + if not uris: + uris = [None] + + expanded = [] + for uri in uris: + type_uris = getTypeURIs(service_element) + expanded.append((type_uris, uri, service_element)) + + return expanded + +def expandServices(service_elements): + """Take a sorted iterator of service elements and expand it into a + sorted iterator of: + ([type_uri], uri, service_element) + + There may be more than one item in the resulting list for each + service element if there is more than one URI or type for a + service, but each triple will be unique. + + If there is no URI or Type for a Service element, it will not + appear in the result. + """ + expanded = [] + for service_element in service_elements: + expanded.extend(expandService(service_element)) + + return expanded diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/filters.py b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/filters.py new file mode 100644 index 0000000..d01c221 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/filters.py @@ -0,0 +1,200 @@ +"""This module contains functions and classes used for extracting +endpoint information out of a Yadis XRD file using the ElementTree XML +parser. +""" + +__all__ = [ + 'BasicServiceEndpoint', + 'mkFilter', + 'IFilter', + 'TransformFilterMaker', + 'CompoundFilter', + ] + +from openid.yadis.etxrd import expandService + +class BasicServiceEndpoint(object): + """Generic endpoint object that contains parsed service + information, as well as a reference to the service element from + which it was generated. If there is more than one xrd:Type or + xrd:URI in the xrd:Service, this object represents just one of + those pairs. + + This object can be used as a filter, because it implements + fromBasicServiceEndpoint. + + The simplest kind of filter you can write implements + fromBasicServiceEndpoint, which takes one of these objects. + """ + def __init__(self, yadis_url, type_uris, uri, service_element): + self.type_uris = type_uris + self.yadis_url = yadis_url + self.uri = uri + self.service_element = service_element + + def matchTypes(self, type_uris): + """Query this endpoint to see if it has any of the given type + URIs. This is useful for implementing other endpoint classes + that e.g. need to check for the presence of multiple versions + of a single protocol. + + @param type_uris: The URIs that you wish to check + @type type_uris: iterable of str + + @return: all types that are in both in type_uris and + self.type_uris + """ + return [uri for uri in type_uris if uri in self.type_uris] + + def fromBasicServiceEndpoint(endpoint): + """Trivial transform from a basic endpoint to itself. This + method exists to allow BasicServiceEndpoint to be used as a + filter. + + If you are subclassing this object, re-implement this function. + + @param endpoint: An instance of BasicServiceEndpoint + @return: The object that was passed in, with no processing. + """ + return endpoint + + fromBasicServiceEndpoint = staticmethod(fromBasicServiceEndpoint) + +class IFilter(object): + """Interface for Yadis filter objects. Other filter-like things + are convertable to this class.""" + + def getServiceEndpoints(self, yadis_url, service_element): + """Returns an iterator of endpoint objects""" + raise NotImplementedError + +class TransformFilterMaker(object): + """Take a list of basic filters and makes a filter that transforms + the basic filter into a top-level filter. This is mostly useful + for the implementation of mkFilter, which should only be needed + for special cases or internal use by this library. + + This object is useful for creating simple filters for services + that use one URI and are specified by one Type (we expect most + Types will fit this paradigm). + + Creates a BasicServiceEndpoint object and apply the filter + functions to it until one of them returns a value. + """ + + def __init__(self, filter_functions): + """Initialize the filter maker's state + + @param filter_functions: The endpoint transformer functions to + apply to the basic endpoint. These are called in turn + until one of them does not return None, and the result of + that transformer is returned. + """ + self.filter_functions = filter_functions + + def getServiceEndpoints(self, yadis_url, service_element): + """Returns an iterator of endpoint objects produced by the + filter functions.""" + endpoints = [] + + # Do an expansion of the service element by xrd:Type and xrd:URI + for type_uris, uri, _ in expandService(service_element): + + # Create a basic endpoint object to represent this + # yadis_url, Service, Type, URI combination + endpoint = BasicServiceEndpoint( + yadis_url, type_uris, uri, service_element) + + e = self.applyFilters(endpoint) + if e is not None: + endpoints.append(e) + + return endpoints + + def applyFilters(self, endpoint): + """Apply filter functions to an endpoint until one of them + returns non-None.""" + for filter_function in self.filter_functions: + e = filter_function(endpoint) + if e is not None: + # Once one of the filters has returned an + # endpoint, do not apply any more. + return e + + return None + +class CompoundFilter(object): + """Create a new filter that applies a set of filters to an endpoint + and collects their results. + """ + def __init__(self, subfilters): + self.subfilters = subfilters + + def getServiceEndpoints(self, yadis_url, service_element): + """Generate all endpoint objects for all of the subfilters of + this filter and return their concatenation.""" + endpoints = [] + for subfilter in self.subfilters: + endpoints.extend( + subfilter.getServiceEndpoints(yadis_url, service_element)) + return endpoints + +# Exception raised when something is not able to be turned into a filter +filter_type_error = TypeError( + 'Expected a filter, an endpoint, a callable or a list of any of these.') + +def mkFilter(parts): + """Convert a filter-convertable thing into a filter + + @param parts: a filter, an endpoint, a callable, or a list of any of these. + """ + # Convert the parts into a list, and pass to mkCompoundFilter + if parts is None: + parts = [BasicServiceEndpoint] + + try: + parts = list(parts) + except TypeError: + return mkCompoundFilter([parts]) + else: + return mkCompoundFilter(parts) + +def mkCompoundFilter(parts): + """Create a filter out of a list of filter-like things + + Used by mkFilter + + @param parts: list of filter, endpoint, callable or list of any of these + """ + # Separate into a list of callables and a list of filter objects + transformers = [] + filters = [] + for subfilter in parts: + try: + subfilter = list(subfilter) + except TypeError: + # If it's not an iterable + if hasattr(subfilter, 'getServiceEndpoints'): + # It's a full filter + filters.append(subfilter) + elif hasattr(subfilter, 'fromBasicServiceEndpoint'): + # It's an endpoint object, so put its endpoint + # conversion attribute into the list of endpoint + # transformers + transformers.append(subfilter.fromBasicServiceEndpoint) + elif callable(subfilter): + # It's a simple callable, so add it to the list of + # endpoint transformers + transformers.append(subfilter) + else: + raise filter_type_error + else: + filters.append(mkCompoundFilter(subfilter)) + + if transformers: + filters.append(TransformFilterMaker(transformers)) + + if len(filters) == 1: + return filters[0] + else: + return CompoundFilter(filters) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/manager.py b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/manager.py new file mode 100644 index 0000000..709adb7 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/manager.py @@ -0,0 +1,194 @@ +class YadisServiceManager(object): + """Holds the state of a list of selected Yadis services, managing + storing it in a session and iterating over the services in order.""" + + def __init__(self, starting_url, yadis_url, services, session_key): + # The URL that was used to initiate the Yadis protocol + self.starting_url = starting_url + + # The URL after following redirects (the identifier) + self.yadis_url = yadis_url + + # List of service elements + self.services = list(services) + + self.session_key = session_key + + # Reference to the current service object + self._current = None + + def __len__(self): + """How many untried services remain?""" + return len(self.services) + + def __iter__(self): + return self + + def next(self): + """Return the next service + + self.current() will continue to return that service until the + next call to this method.""" + try: + self._current = self.services.pop(0) + except IndexError: + raise StopIteration + else: + return self._current + + def current(self): + """Return the current service. + + Returns None if there are no services left. + """ + return self._current + + def forURL(self, url): + return url in [self.starting_url, self.yadis_url] + + def started(self): + """Has the first service been returned?""" + return self._current is not None + + def store(self, session): + """Store this object in the session, by its session key.""" + session[self.session_key] = self + +class Discovery(object): + """State management for discovery. + + High-level usage pattern is to call .getNextService(discover) in + order to find the next available service for this user for this + session. Once a request completes, call .finish() to clean up the + session state. + + @ivar session: a dict-like object that stores state unique to the + requesting user-agent. This object must be able to store + serializable objects. + + @ivar url: the URL that is used to make the discovery request + + @ivar session_key_suffix: The suffix that will be used to identify + this object in the session object. + """ + + DEFAULT_SUFFIX = 'auth' + PREFIX = '_yadis_services_' + + def __init__(self, session, url, session_key_suffix=None): + """Initialize a discovery object""" + self.session = session + self.url = url + if session_key_suffix is None: + session_key_suffix = self.DEFAULT_SUFFIX + + self.session_key_suffix = session_key_suffix + + def getNextService(self, discover): + """Return the next authentication service for the pair of + user_input and session. This function handles fallback. + + + @param discover: a callable that takes a URL and returns a + list of services + + @type discover: str -> [service] + + + @return: the next available service + """ + manager = self.getManager() + if manager is not None and not manager: + self.destroyManager() + + if not manager: + yadis_url, services = discover(self.url) + manager = self.createManager(services, yadis_url) + + if manager: + service = manager.next() + manager.store(self.session) + else: + service = None + + return service + + def cleanup(self, force=False): + """Clean up Yadis-related services in the session and return + the most-recently-attempted service from the manager, if one + exists. + + @param force: True if the manager should be deleted regardless + of whether it's a manager for self.url. + + @return: current service endpoint object or None if there is + no current service + """ + manager = self.getManager(force=force) + if manager is not None: + service = manager.current() + self.destroyManager(force=force) + else: + service = None + + return service + + ### Lower-level methods + + def getSessionKey(self): + """Get the session key for this starting URL and suffix + + @return: The session key + @rtype: str + """ + return self.PREFIX + self.session_key_suffix + + def getManager(self, force=False): + """Extract the YadisServiceManager for this object's URL and + suffix from the session. + + @param force: True if the manager should be returned + regardless of whether it's a manager for self.url. + + @return: The current YadisServiceManager, if it's for this + URL, or else None + """ + manager = self.session.get(self.getSessionKey()) + if (manager is not None and (manager.forURL(self.url) or force)): + return manager + else: + return None + + def createManager(self, services, yadis_url=None): + """Create a new YadisService Manager for this starting URL and + suffix, and store it in the session. + + @raises KeyError: When I already have a manager. + + @return: A new YadisServiceManager or None + """ + key = self.getSessionKey() + if self.getManager(): + raise KeyError('There is already a %r manager for %r' % + (key, self.url)) + + if not services: + return None + + manager = YadisServiceManager(self.url, yadis_url, services, key) + manager.store(self.session) + return manager + + def destroyManager(self, force=False): + """Delete any YadisServiceManager with this starting URL and + suffix from the session. + + If there is no service manager or the service manager is for a + different URL, it silently does nothing. + + @param force: True if the manager should be deleted regardless + of whether it's a manager for self.url. + """ + if self.getManager(force=force) is not None: + key = self.getSessionKey() + del self.session[key] diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/parsehtml.py b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/parsehtml.py new file mode 100644 index 0000000..875a089 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/parsehtml.py @@ -0,0 +1,197 @@ +__all__ = ['findHTMLMeta', 'MetaNotFound'] + +from HTMLParser import HTMLParser, HTMLParseError +import htmlentitydefs +import re + +from openid.yadis.constants import YADIS_HEADER_NAME + +# Size of the chunks to search at a time (also the amount that gets +# read at a time) +CHUNK_SIZE = 1024 * 16 # 16 KB + +class ParseDone(Exception): + """Exception to hold the URI that was located when the parse is + finished. If the parse finishes without finding the URI, set it to + None.""" + +class MetaNotFound(Exception): + """Exception to hold the content of the page if we did not find + the appropriate tag""" + +re_flags = re.IGNORECASE | re.UNICODE | re.VERBOSE +ent_pat = r''' +& + +(?: \#x (?P [a-f0-9]+ ) +| \# (?P \d+ ) +| (?P \w+ ) +) + +;''' + +ent_re = re.compile(ent_pat, re_flags) + +def substituteMO(mo): + if mo.lastgroup == 'hex': + codepoint = int(mo.group('hex'), 16) + elif mo.lastgroup == 'dec': + codepoint = int(mo.group('dec')) + else: + assert mo.lastgroup == 'word' + codepoint = htmlentitydefs.name2codepoint.get(mo.group('word')) + + if codepoint is None: + return mo.group() + else: + return unichr(codepoint) + +def substituteEntities(s): + return ent_re.sub(substituteMO, s) + +class YadisHTMLParser(HTMLParser): + """Parser that finds a meta http-equiv tag in the head of a html + document. + + When feeding in data, if the tag is matched or it will never be + found, the parser will raise ParseDone with the uri as the first + attribute. + + Parsing state diagram + ===================== + + Any unlisted input does not affect the state:: + + 1, 2, 5 8 + +--------------------------+ +-+ + | | | | + 4 | 3 1, 2, 5, 7 v | v + TOP -> HTML -> HEAD ----------> TERMINATED + | | ^ | ^ ^ + | | 3 | | | | + | +------------+ +-> FOUND ------+ | + | 6 8 | + | 1, 2 | + +------------------------------------+ + + 1. any of , , -> TERMINATE + 2. -> TERMINATE + 3. -> HEAD + 4. -> HTML + 5. -> TERMINATE + 6. -> FOUND + 7. -> TERMINATE + 8. Any input -> TERMINATE + """ + TOP = 0 + HTML = 1 + HEAD = 2 + FOUND = 3 + TERMINATED = 4 + + def __init__(self): + HTMLParser.__init__(self) + self.phase = self.TOP + + def _terminate(self): + self.phase = self.TERMINATED + raise ParseDone(None) + + def handle_endtag(self, tag): + # If we ever see an end of head, body, or html, bail out right away. + # [1] + if tag in ['head', 'body', 'html']: + self._terminate() + + def handle_starttag(self, tag, attrs): + # if we ever see a start body tag, bail out right away, since + # we want to prevent the meta tag from appearing in the body + # [2] + if tag=='body': + self._terminate() + + if self.phase == self.TOP: + # At the top level, allow a html tag or a head tag to move + # to the head or html phase + if tag == 'head': + # [3] + self.phase = self.HEAD + elif tag == 'html': + # [4] + self.phase = self.HTML + + elif self.phase == self.HTML: + # if we are in the html tag, allow a head tag to move to + # the HEAD phase. If we get another html tag, then bail + # out + if tag == 'head': + # [3] + self.phase = self.HEAD + elif tag == 'html': + # [5] + self._terminate() + + elif self.phase == self.HEAD: + # If we are in the head phase, look for the appropriate + # meta tag. If we get a head or body tag, bail out. + if tag == 'meta': + attrs_d = dict(attrs) + http_equiv = attrs_d.get('http-equiv', '').lower() + if http_equiv == YADIS_HEADER_NAME.lower(): + raw_attr = attrs_d.get('content') + yadis_loc = substituteEntities(raw_attr) + # [6] + self.phase = self.FOUND + raise ParseDone(yadis_loc) + + elif tag in ['head', 'html']: + # [5], [7] + self._terminate() + + def feed(self, chars): + # [8] + if self.phase in [self.TERMINATED, self.FOUND]: + self._terminate() + + return HTMLParser.feed(self, chars) + +def findHTMLMeta(stream): + """Look for a meta http-equiv tag with the YADIS header name. + + @param stream: Source of the html text + @type stream: Object that implements a read() method that works + like file.read + + @return: The URI from which to fetch the XRDS document + @rtype: str + + @raises MetaNotFound: raised with the content that was + searched as the first parameter. + """ + parser = YadisHTMLParser() + chunks = [] + + while 1: + chunk = stream.read(CHUNK_SIZE) + if not chunk: + # End of file + break + + chunks.append(chunk) + try: + parser.feed(chunk) + except HTMLParseError, why: + # HTML parse error, so bail + chunks.append(stream.read()) + break + except ParseDone, why: + uri = why[0] + if uri is None: + # Parse finished, but we may need the rest of the file + chunks.append(stream.read()) + break + else: + return uri + + content = ''.join(chunks) + raise MetaNotFound(content) diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/services.py b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/services.py new file mode 100644 index 0000000..4753c19 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/services.py @@ -0,0 +1,54 @@ +# -*- test-case-name: openid.test.test_services -*- + +from openid.yadis.filters import mkFilter +from openid.yadis.discover import discover, DiscoveryFailure +from openid.yadis.etxrd import parseXRDS, iterServices, XRDSError + +def getServiceEndpoints(input_url, flt=None): + """Perform the Yadis protocol on the input URL and return an + iterable of resulting endpoint objects. + + @param flt: A filter object or something that is convertable to + a filter object (using mkFilter) that will be used to generate + endpoint objects. This defaults to generating BasicEndpoint + objects. + + @param input_url: The URL on which to perform the Yadis protocol + + @return: The normalized identity URL and an iterable of endpoint + objects generated by the filter function. + + @rtype: (str, [endpoint]) + + @raises DiscoveryFailure: when Yadis fails to obtain an XRDS document. + """ + result = discover(input_url) + try: + endpoints = applyFilter(result.normalized_uri, + result.response_text, flt) + except XRDSError, err: + raise DiscoveryFailure(str(err), None) + return (result.normalized_uri, endpoints) + +def applyFilter(normalized_uri, xrd_data, flt=None): + """Generate an iterable of endpoint objects given this input data, + presumably from the result of performing the Yadis protocol. + + @param normalized_uri: The input URL, after following redirects, + as in the Yadis protocol. + + + @param xrd_data: The XML text the XRDS file fetched from the + normalized URI. + @type xrd_data: str + + """ + flt = mkFilter(flt) + et = parseXRDS(xrd_data) + + endpoints = [] + for service_element in iterServices(et): + endpoints.extend( + flt.getServiceEndpoints(normalized_uri, service_element)) + + return endpoints diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/xri.py b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/xri.py new file mode 100644 index 0000000..3a39a6b --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/xri.py @@ -0,0 +1,168 @@ +# -*- test-case-name: openid.test.test_xri -*- +"""Utility functions for handling XRIs. + +@see: XRI Syntax v2.0 at the U{OASIS XRI Technical Committee} +""" + +import re + +XRI_AUTHORITIES = ['!', '=', '@', '+', '$', '('] + +try: + unichr(0x10000) +except ValueError: + # narrow python build + UCSCHAR = [ + (0xA0, 0xD7FF), + (0xF900, 0xFDCF), + (0xFDF0, 0xFFEF), + ] + + IPRIVATE = [ + (0xE000, 0xF8FF), + ] +else: + UCSCHAR = [ + (0xA0, 0xD7FF), + (0xF900, 0xFDCF), + (0xFDF0, 0xFFEF), + (0x10000, 0x1FFFD), + (0x20000, 0x2FFFD), + (0x30000, 0x3FFFD), + (0x40000, 0x4FFFD), + (0x50000, 0x5FFFD), + (0x60000, 0x6FFFD), + (0x70000, 0x7FFFD), + (0x80000, 0x8FFFD), + (0x90000, 0x9FFFD), + (0xA0000, 0xAFFFD), + (0xB0000, 0xBFFFD), + (0xC0000, 0xCFFFD), + (0xD0000, 0xDFFFD), + (0xE1000, 0xEFFFD), + ] + + IPRIVATE = [ + (0xE000, 0xF8FF), + (0xF0000, 0xFFFFD), + (0x100000, 0x10FFFD), + ] + + +_escapeme_re = re.compile('[%s]' % (''.join( + map(lambda (m, n): u'%s-%s' % (unichr(m), unichr(n)), + UCSCHAR + IPRIVATE)),)) + + +def identifierScheme(identifier): + """Determine if this identifier is an XRI or URI. + + @returns: C{"XRI"} or C{"URI"} + """ + if identifier.startswith('xri://') or ( + identifier and identifier[0] in XRI_AUTHORITIES): + return "XRI" + else: + return "URI" + + +def toIRINormal(xri): + """Transform an XRI to IRI-normal form.""" + if not xri.startswith('xri://'): + xri = 'xri://' + xri + return escapeForIRI(xri) + + +_xref_re = re.compile('\((.*?)\)') + + +def _escape_xref(xref_match): + """Escape things that need to be escaped if they're in a cross-reference. + """ + xref = xref_match.group() + xref = xref.replace('/', '%2F') + xref = xref.replace('?', '%3F') + xref = xref.replace('#', '%23') + return xref + + +def escapeForIRI(xri): + """Escape things that need to be escaped when transforming to an IRI.""" + xri = xri.replace('%', '%25') + xri = _xref_re.sub(_escape_xref, xri) + return xri + + +def toURINormal(xri): + """Transform an XRI to URI normal form.""" + return iriToURI(toIRINormal(xri)) + + +def _percentEscapeUnicode(char_match): + c = char_match.group() + return ''.join(['%%%X' % (ord(octet),) for octet in c.encode('utf-8')]) + + +def iriToURI(iri): + """Transform an IRI to a URI by escaping unicode.""" + # According to RFC 3987, section 3.1, "Mapping of IRIs to URIs" + return _escapeme_re.sub(_percentEscapeUnicode, iri) + + +def providerIsAuthoritative(providerID, canonicalID): + """Is this provider ID authoritative for this XRI? + + @returntype: bool + """ + # XXX: can't use rsplit until we require python >= 2.4. + lastbang = canonicalID.rindex('!') + parent = canonicalID[:lastbang] + return parent == providerID + + +def rootAuthority(xri): + """Return the root authority for an XRI. + + Example:: + + rootAuthority("xri://@example") == "xri://@" + + @type xri: unicode + @returntype: unicode + """ + if xri.startswith('xri://'): + xri = xri[6:] + authority = xri.split('/', 1)[0] + if authority[0] == '(': + # Cross-reference. + # XXX: This is incorrect if someone nests cross-references so there + # is another close-paren in there. Hopefully nobody does that + # before we have a real xriparse function. Hopefully nobody does + # that *ever*. + root = authority[:authority.index(')') + 1] + elif authority[0] in XRI_AUTHORITIES: + # Other XRI reference. + root = authority[0] + else: + # IRI reference. XXX: Can IRI authorities have segments? + segments = authority.split('!') + segments = reduce(list.__add__, + map(lambda s: s.split('*'), segments)) + root = segments[0] + + return XRI(root) + + +def XRI(xri): + """An XRI object allowing comparison of XRI. + + Ideally, this would do full normalization and provide comparsion + operators as per XRI Syntax. Right now, it just does a bit of + canonicalization by ensuring the xri scheme is present. + + @param xri: an xri string + @type xri: unicode + """ + if not xri.startswith('xri://'): + xri = 'xri://' + xri + return xri diff --git a/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/xrires.py b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/xrires.py new file mode 100644 index 0000000..be663c6 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/openid/yadis/xrires.py @@ -0,0 +1,123 @@ +# -*- test-case-name: openid.test.test_xrires -*- +"""XRI resolution. +""" + +from urllib import urlencode +from openid import fetchers +from openid.yadis import etxrd +from openid.yadis.xri import toURINormal +from openid.yadis.services import iterServices + +DEFAULT_PROXY = 'http://proxy.xri.net/' + +class ProxyResolver(object): + """Python interface to a remote XRI proxy resolver. + """ + def __init__(self, proxy_url=DEFAULT_PROXY): + self.proxy_url = proxy_url + + + def queryURL(self, xri, service_type=None): + """Build a URL to query the proxy resolver. + + @param xri: An XRI to resolve. + @type xri: unicode + + @param service_type: The service type to resolve, if you desire + service endpoint selection. A service type is a URI. + @type service_type: str + + @returns: a URL + @returntype: str + """ + # Trim off the xri:// prefix. The proxy resolver didn't accept it + # when this code was written, but that may (or may not) change for + # XRI Resolution 2.0 Working Draft 11. + qxri = toURINormal(xri)[6:] + hxri = self.proxy_url + qxri + args = { + # XXX: If the proxy resolver will ensure that it doesn't return + # bogus CanonicalIDs (as per Steve's message of 15 Aug 2006 + # 11:13:42), then we could ask for application/xrd+xml instead, + # which would give us a bit less to process. + '_xrd_r': 'application/xrds+xml', + } + if service_type: + args['_xrd_t'] = service_type + else: + # Don't perform service endpoint selection. + args['_xrd_r'] += ';sep=false' + query = _appendArgs(hxri, args) + return query + + + def query(self, xri, service_types): + """Resolve some services for an XRI. + + Note: I don't implement any service endpoint selection beyond what + the resolver I'm querying does, so the Services I return may well + include Services that were not of the types you asked for. + + May raise fetchers.HTTPFetchingError or L{etxrd.XRDSError} if + the fetching or parsing don't go so well. + + @param xri: An XRI to resolve. + @type xri: unicode + + @param service_types: A list of services types to query for. Service + types are URIs. + @type service_types: list of str + + @returns: tuple of (CanonicalID, Service elements) + @returntype: (unicode, list of C{ElementTree.Element}s) + """ + # FIXME: No test coverage! + services = [] + # Make a seperate request to the proxy resolver for each service + # type, as, if it is following Refs, it could return a different + # XRDS for each. + + canonicalID = None + + for service_type in service_types: + url = self.queryURL(xri, service_type) + response = fetchers.fetch(url) + if response.status not in (200, 206): + # XXX: sucks to fail silently. + # print "response not OK:", response + continue + et = etxrd.parseXRDS(response.body) + canonicalID = etxrd.getCanonicalID(xri, et) + some_services = list(iterServices(et)) + services.extend(some_services) + # TODO: + # * If we do get hits for multiple service_types, we're almost + # certainly going to have duplicated service entries and + # broken priority ordering. + return canonicalID, services + + +def _appendArgs(url, args): + """Append some arguments to an HTTP query. + """ + # to be merged with oidutil.appendArgs when we combine the projects. + if hasattr(args, 'items'): + args = args.items() + args.sort() + + if len(args) == 0: + return url + + # According to XRI Resolution section "QXRI query parameters": + # + # """If the original QXRI had a null query component (only a leading + # question mark), or a query component consisting of only question + # marks, one additional leading question mark MUST be added when + # adding any XRI resolution parameters.""" + + if '?' in url.rstrip('?'): + sep = '&' + else: + sep = '?' + + return '%s%s%s' % (url, sep, urlencode(args)) diff --git a/desktop/core/ext-py/python-openid-2.2.5/setup.cfg b/desktop/core/ext-py/python-openid-2.2.5/setup.cfg new file mode 100644 index 0000000..5706708 --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/setup.cfg @@ -0,0 +1,3 @@ +[sdist] +force_manifest=1 +formats=gztar,zip diff --git a/desktop/core/ext-py/python-openid-2.2.5/setup.py b/desktop/core/ext-py/python-openid-2.2.5/setup.py new file mode 100644 index 0000000..b9bf02b --- /dev/null +++ b/desktop/core/ext-py/python-openid-2.2.5/setup.py @@ -0,0 +1,46 @@ +import sys +import os + +from distutils.core import setup + +if 'sdist' in sys.argv: + os.system('./admin/makedoc') + +version = '[library version:2.2.5]'[17:-1] + +setup( + name='python-openid', + version=version, + description='OpenID support for servers and consumers.', + long_description='''This is a set of Python packages to support use of +the OpenID decentralized identity system in your application. Want to enable +single sign-on for your web site? Use the openid.consumer package. Want to +run your own OpenID server? Check out openid.server. Includes example code +and support for a variety of storage back-ends.''', + url='http://github.com/openid/python-openid', + packages=['openid', + 'openid.consumer', + 'openid.server', + 'openid.store', + 'openid.yadis', + 'openid.extensions', + 'openid.extensions.draft', + ], + # license specified by classifier. + # license=getLicense(), + author='JanRain', + author_email='openid@janrain.com', + download_url='http://github.com/openid/python-openid/tarball/%s' % (version,), + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: POSIX", + "Programming Language :: Python", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Systems Administration :: Authentication/Directory", + ], + ) diff --git a/desktop/core/src/desktop/settings.py b/desktop/core/src/desktop/settings.py index 9bc2c3b..59cb388 100644 --- a/desktop/core/src/desktop/settings.py +++ b/desktop/core/src/desktop/settings.py @@ -142,6 +142,7 @@ TEMPLATE_DIRS = ( INSTALLED_APPS = [ 'django.contrib.auth', + 'django_openid_auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', @@ -311,6 +312,14 @@ if SAML_AUTHENTICATION: LOGIN_URL = '/saml2/login/' SESSION_EXPIRE_AT_BROWSER_CLOSE = True +# OpenId +OPENID_AUTHENTICATION = 'libopenid.backend.OpenIDBackend' in AUTHENTICATION_BACKENDS +if OPENID_AUTHENTICATION: + from libopenid.openid_settings import * + INSTALLED_APPS.append('libopenid') + LOGIN_URL = '/openid/login' + SESSION_EXPIRE_AT_BROWSER_CLOSE = True + # URL Redirection white list. if desktop.conf.REDIRECT_WHITELIST.get(): MIDDLEWARE_CLASSES.append('desktop.middleware.EnsureSafeRedirectURLMiddleware') diff --git a/desktop/core/src/desktop/urls.py b/desktop/core/src/desktop/urls.py index 89c6399..da794be 100644 --- a/desktop/core/src/desktop/urls.py +++ b/desktop/core/src/desktop/urls.py @@ -97,6 +97,10 @@ static_patterns = [] if settings.SAML_AUTHENTICATION: static_patterns.append((r'^saml2/', include('libsaml.urls'))) +# OpenId specific +if settings.OPENID_AUTHENTICATION: + static_patterns.append((r'^openid/', include('libopenid.urls'))) + # Root each app at /appname if they have a "urls" module for app in appmanager.DESKTOP_APPS: if app.urls: diff --git a/desktop/libs/libopenid/Makefile b/desktop/libs/libopenid/Makefile new file mode 100644 index 0000000..1bda9cf --- /dev/null +++ b/desktop/libs/libopenid/Makefile @@ -0,0 +1,36 @@ +# +# Licensed to Cloudera, Inc. under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. Cloudera, Inc. licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +ifeq ($(ROOT),) + $(error "Error: Expect the environment variable $$ROOT to point to the Desktop installation") +endif + +include $(ROOT)/Makefile.sdk + +default:: + @echo ' env-install : Install into virtual-env' + +# +# env-install +# Install app into the virtual environment. +# +.PHONY: env-install +env-install: compile ext-env-install + @echo '--- Installing $(APP_NAME) into virtual-env' + @$(ENV_PYTHON) setup.py develop -N -q diff --git a/desktop/libs/libopenid/babel.cfg b/desktop/libs/libopenid/babel.cfg new file mode 100644 index 0000000..121c0c9 --- /dev/null +++ b/desktop/libs/libopenid/babel.cfg @@ -0,0 +1,2 @@ +[python: src/libopenid/**.py] +[mako: src/libopenid/templates/**.mako] diff --git a/desktop/libs/libopenid/setup.py b/desktop/libs/libopenid/setup.py new file mode 100644 index 0000000..26bd479 --- /dev/null +++ b/desktop/libs/libopenid/setup.py @@ -0,0 +1,30 @@ +# Licensed to Cloudera, Inc. under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. Cloudera, Inc. licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from setuptools import setup, find_packages + +setup( + name = "libopenid", + version = "1.0", + url = 'http://github.com/cloudera/hue', + description = "OPENID Libraries", + packages = find_packages('src'), + package_dir = {'': 'src' }, + install_requires = ['setuptools', 'desktop'], + # Even libraries need to be registered as desktop_apps, + # if they have configuration, like this one. + entry_points = { 'desktop.sdk.lib': 'libopenid=libopenid' }, +) diff --git a/desktop/libs/libopenid/src/libopenid/__init__.py b/desktop/libs/libopenid/src/libopenid/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/desktop/libs/libopenid/src/libopenid/backend.py b/desktop/libs/libopenid/src/libopenid/backend.py new file mode 100644 index 0000000..128fedd --- /dev/null +++ b/desktop/libs/libopenid/src/libopenid/backend.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +# Licensed to Cloudera, Inc. under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. Cloudera, Inc. licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +See desktop/auth/backend.py +""" +import logging +import sys +from django.contrib.auth import logout as auth_logout +from django.contrib.auth.models import User +from django_openid_auth.auth import OpenIDBackend as _OpenIDBackend +from desktop.auth.backend import rewrite_user +from useradmin.models import get_profile, get_default_user_group, UserProfile + + +LOG = logging.getLogger(__name__) + + +class OpenIDBackend(_OpenIDBackend): + """ + Wrapper around openid backend. + """ + def update_user_details(self, user, details, openid_response): + # Do this check up here, because the auth call creates a django user upon first login per user + is_super = False + + if not UserProfile.objects.filter(creation_method=str(UserProfile.CreationMethod.EXTERNAL)).exists(): + # If there are no external users already in the system, the first one will + # become a superuser + is_super = True + elif User.objects.filter(username=user.username).exists(): + # If the user already exists, we shouldn't change its superuser + # privileges. However, if there's a naming conflict with a non-external + # user, we should do the safe thing and turn off superuser privs. + user = User.objects.get(username=user.username) + existing_profile = get_profile(user) + if existing_profile.creation_method == str(UserProfile.CreationMethod.EXTERNAL): + is_super = user.is_superuser + + + super(OpenIDBackend, self).update_user_details(user, details, openid_response) + + + if user is not None and user.is_active: + profile = get_profile(user) + profile.creation_method = UserProfile.CreationMethod.EXTERNAL + profile.save() + user.is_superuser = is_super + user = rewrite_user(user) + + default_group = get_default_user_group() + if default_group is not None: + user.groups.add(default_group) + user.save() + + + + def get_user(self, user_id): + user = super(OpenIDBackend, self).get_user(user_id) + user = rewrite_user(user) + return user + + @classmethod + def manages_passwords_externally(cls): + return True + + @classmethod + def is_first_login_ever(cls): + """ Return true if no external user has ever logged in to Desktop yet. """ + return not UserProfile.objects.filter(creation_method=str(UserProfile.CreationMethod.EXTERNAL)).exists() + diff --git a/desktop/libs/libopenid/src/libopenid/conf.py b/desktop/libs/libopenid/src/libopenid/conf.py new file mode 100644 index 0000000..2e28694 --- /dev/null +++ b/desktop/libs/libopenid/src/libopenid/conf.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# Licensed to Cloudera, Inc. under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. Cloudera, Inc. licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json +import os + +from django.utils.translation import ugettext_lazy as _t, ugettext as _ + +from desktop.lib.conf import Config, coerce_bool, coerce_csv + + +BASEDIR = os.path.dirname(os.path.abspath(__file__)) + + +def dict_list_map(value): + if isinstance(value, str): + d = {} + for k, v in json.loads(value).iteritems(): + d[k] = (v,) + return d + elif isinstance(value, dict): + return value + return None + + +SERVER_ENDPOINT_URL = Config( + key="server_endpoint_url", + default=None, + type=str, + help="OpenId SSO endpoint url") + +IDENTITY_URL_PREFIX = Config( + key="identity_url_prefix", + default=None, + type=str, + help="Openid identity url prefix") + + +CREATE_USERS_ON_LOGIN = Config( + key="create_users_on_login", + default=True, + type=coerce_bool, + help=_t("Create users from IdP on login.")) + +USE_EMAIL_FOR_USERNAME = Config( + key="use_email_for_username", + default=True, + type=coerce_bool, + help=_t("Use email for username.")) + +def config_validator(user): + res = [] + if not SERVER_ENDPOINT_URL.get(): + res.append(("libopenid.server_endpoint_url", _("Required OPENID SSO endpoint URL is not provided."))) + return res diff --git a/desktop/libs/libopenid/src/libopenid/forms.py b/desktop/libs/libopenid/src/libopenid/forms.py new file mode 100644 index 0000000..a0656e5 --- /dev/null +++ b/desktop/libs/libopenid/src/libopenid/forms.py @@ -0,0 +1,14 @@ +from django_openid_auth.forms import OpenIDLoginForm +from django.conf import settings + +class OpenIDLoginFormExt(OpenIDLoginForm): + + def clean_openid_identifier(self): + openid_identifier = super(OpenIDLoginFormExt, self).clean_openid_identifier() + identity_url_prefix = getattr(settings, 'OPENID_IDENTITY_URL_PREFIX', None) + + #Case of non centralized endpoint POST request with identity prefix + if identity_url_prefix is not None and not (openid_identifier.startswith('http') or openid_identifier.startswith('https')): + openid_identifier = identity_url_prefix + openid_identifier + + return openid_identifier diff --git a/desktop/libs/libopenid/src/libopenid/locale/en/LC_MESSAGES/django.po b/desktop/libs/libopenid/src/libopenid/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000..af8b88c --- /dev/null +++ b/desktop/libs/libopenid/src/libopenid/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,28 @@ +# English translations for Hue. +# Copyright (C) 2014 Cloudera, Inc +# This file is distributed under the same license as the Hue project. +# FIRST AUTHOR , 2014. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Hue VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2014-01-08 17:53+0000\n" +"PO-Revision-Date: 2014-01-03 20:09+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: en \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: src/libopenid/conf.py:56 +msgid "Create users from IdP on login." +msgstr "" + +#: src/libopenid/conf.py:62 +msgid "Use email for username." +msgstr "" + diff --git a/desktop/libs/libopenid/src/libopenid/locale/en_US.pot b/desktop/libs/libopenid/src/libopenid/locale/en_US.pot new file mode 100644 index 0000000..a3233b1 --- /dev/null +++ b/desktop/libs/libopenid/src/libopenid/locale/en_US.pot @@ -0,0 +1,27 @@ +# Translations template for Hue. +# Copyright (C) 2014 Cloudera, Inc +# This file is distributed under the same license as the Hue project. +# FIRST AUTHOR , 2014. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Hue VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2014-01-08 17:53+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.6\n" + +#: src/libopenid/conf.py:56 +msgid "Create users from IdP on login." +msgstr "" + +#: src/libopenid/conf.py:62 +msgid "Use email for username." +msgstr "" + diff --git a/desktop/libs/libopenid/src/libopenid/openid_settings.py b/desktop/libs/libopenid/src/libopenid/openid_settings.py new file mode 100644 index 0000000..fe80170 --- /dev/null +++ b/desktop/libs/libopenid/src/libopenid/openid_settings.py @@ -0,0 +1,45 @@ +# Licensed to Cloudera, Inc. under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. Cloudera, Inc. licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import django_openid_auth +import desktop.conf +import libopenid.conf +import logging + +from desktop.lib import security_util + + + +__all__ = ['OPENID_CONFIG', 'OPENID_CREATE_USERS', 'OPENID_SSO_SERVER_URL', 'OPENID_USE_EMAIL_FOR_USERNAME', 'OPENID_IDENTITY_URL_PREFIX'] + + + + +OPENID_CONFIG = { + + # set to 1 to output debugging information + 'debug': 1, + +} + +# openid sso endpoint url +OPENID_SSO_SERVER_URL = libopenid.conf.SERVER_ENDPOINT_URL.get(); + +OPENID_CREATE_USERS = libopenid.conf.CREATE_USERS_ON_LOGIN.get(); + +OPENID_USE_EMAIL_FOR_USERNAME = libopenid.conf.USE_EMAIL_FOR_USERNAME.get(); + +OPENID_IDENTITY_URL_PREFIX = libopenid.conf.IDENTITY_URL_PREFIX.get(); diff --git a/desktop/libs/libopenid/src/libopenid/templates/openid-login.html b/desktop/libs/libopenid/src/libopenid/templates/openid-login.html new file mode 100644 index 0000000..33bd439 --- /dev/null +++ b/desktop/libs/libopenid/src/libopenid/templates/openid-login.html @@ -0,0 +1,175 @@ +{% load i18n %} + + + + + {% if first_login_ever %} + {% trans "Hue - Sign up" %} + {% else %} + {% trans "Hue - Sign in"%} + {% endif %} + + + + + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + + + + + + diff --git a/desktop/libs/libopenid/src/libopenid/urls.py b/desktop/libs/libopenid/src/libopenid/urls.py new file mode 100644 index 0000000..4024fa7 --- /dev/null +++ b/desktop/libs/libopenid/src/libopenid/urls.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +# Licensed to Cloudera, Inc. under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. Cloudera, Inc. licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from django.conf.urls.defaults import patterns, url + +urlpatterns = patterns( + 'libopenid.views', + url(r'^login/$', 'login_begin', name='openid-login'), + url(r'^complete/$', 'login_complete', name='openid-complete') +) diff --git a/desktop/libs/libopenid/src/libopenid/views.py b/desktop/libs/libopenid/src/libopenid/views.py new file mode 100644 index 0000000..a5333bc --- /dev/null +++ b/desktop/libs/libopenid/src/libopenid/views.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# Licensed to Cloudera, Inc. under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. Cloudera, Inc. licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from django_openid_auth.views import login_begin as django_login_begin, login_complete + +from desktop.lib.django_util import render +from django.core import urlresolvers +import libopenid.conf +from libopenid.backend import OpenIDBackend +from libopenid.forms import OpenIDLoginFormExt +from django.conf import settings +import logging +from django.shortcuts import render_to_response +from django.template import RequestContext + +__all__ = ['login_begin', 'login_complete'] + + + +def login_begin(request): + redirect_to = request.REQUEST.get('next', '/') + is_first_login_ever = OpenIDBackend.is_first_login_ever() + + request.session.set_test_cookie() + + openid_url = getattr(settings, 'OPENID_SSO_SERVER_URL', None) + identity_url_prefix = getattr(settings, 'OPENID_IDENTITY_URL_PREFIX', None) + + #Case of centralized server endpoint Get request + if openid_url is not None: + if request.method == 'GET': + return render_to_response('openid-login.html', { + 'action': urlresolvers.reverse('openid-login'), + 'next': redirect_to, + 'first_login_ever': is_first_login_ever, + 'hide_field': True + }, context_instance=RequestContext(request)) + + + + return django_login_begin(request, template_name='openid-login.html', form_class = OpenIDLoginFormExt) + + + + + + +setattr(login_begin, 'login_notrequired', True) +setattr(login_complete, 'login_notrequired', True) -- 1.8.4